commit 02b1be44f8bbcfcef81833adfb5c6d47abe12766 Author: admin Date: Wed Mar 19 11:55:34 2025 -0700 Add new files for icons, images, documentation, configuration, and initial code structure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2bfceab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + // ...existing code... + + // Live Server Configuration + "liveServer.settings.root": "/data", + "liveServer.settings.port": 5500, + "liveServer.settings.cors": true, + "liveServer.settings.mount": [ + ["/", "./data"] + ], + "liveServer.settings.CustomBrowser": "chrome", + "liveServer.settings.NoBrowser": false, + "liveServer.settings.ignoreFiles": [ + ".vscode/**", + "**/*.scss", + "**/*.sass" + ], + "liveServer.settings.wait": 100, + "liveServer.settings.host": "localhost" +} \ No newline at end of file diff --git a/Backup/data/Test1.txt b/Backup/data/Test1.txt new file mode 100644 index 0000000..fad425a --- /dev/null +++ b/Backup/data/Test1.txt @@ -0,0 +1 @@ +asdfada \ No newline at end of file diff --git a/Backup/data/Test2.txt b/Backup/data/Test2.txt new file mode 100644 index 0000000..be80832 --- /dev/null +++ b/Backup/data/Test2.txt @@ -0,0 +1,8 @@ +adfasdfadfa +adfasdfadfaadfa +sd + +adfasdfadfaadfaa + + +afas \ No newline at end of file diff --git a/Backup/data/anim-profiles.json b/Backup/data/anim-profiles.json new file mode 100644 index 0000000..e949d44 --- /dev/null +++ b/Backup/data/anim-profiles.json @@ -0,0 +1,499 @@ +{ + "countdown": { + "min": "10", + "max": "75", + "hold": "700", + "ramp": "650" + }, + "profile-index": 0, + "profiles": [ + { + "name": "Profile test", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": "5", + "speed": "10" + }, + { + "anim": 1, + "colmain": "#000020", + "colbase": "#000020", + "density": "25", + "speed": "50" + }, + { + "anim": 2, + "colmain": "#000020", + "colbase": "#000020", + "density": "35", + "speed": "65" + }, + { + "anim": 3, + "colmain": "#000020", + "colbase": "#000020", + "density": "45", + "speed": "75" + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": "5", + "speed": "10" + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": "5", + "speed": "10" + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": "5", + "speed": "10" + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": "5", + "speed": "70" + } + ] + }, + { + "name": "Profile 1", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 2", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 3", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 4", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 5", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 6", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 7", + "events": [ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 1, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 70 + } + ] + } + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/!instructions.txt b/Backup/data/cfg/!instructions.txt new file mode 100644 index 0000000..5301313 --- /dev/null +++ b/Backup/data/cfg/!instructions.txt @@ -0,0 +1,15 @@ + +Choose Control App: + 1) Open app-events.json + 2) Change index to corresponding app + a. 0=DSLRBooth, 1=...., 2=..... + + +Front Constant Light: ( to enable the light) + 1) Open led-devices.json + 2) Change "front-light", "en" to true + + +Rear Constant Light: + 1) Open led-devices.json + 2) Change "rear-light", "en" to true \ No newline at end of file diff --git a/Backup/data/cfg/anim-list.json b/Backup/data/cfg/anim-list.json new file mode 100644 index 0000000..87bc231 --- /dev/null +++ b/Backup/data/cfg/anim-list.json @@ -0,0 +1,25 @@ +{ + "whitefills":[ + "Bottom/Up Fill", + "Snake Fill", + "Tick Fill", + "Smooth Brighten", + "Cycle All", + "Random Select" + ], + "animations":[ + "Rainbow", + "Hue Spectrum Mirrored", + "Meteors Hue", + "Dashes", + "Sectors", + "Fire (red) mirrored", + "Fire (blue) mirrored", + "Fire (green) mirrored", + "Strobe", + "Twinkle", + "Rain", + "Stacking", + "Snake (Hue Range)" + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/anim-profiles.json b/Backup/data/cfg/anim-profiles.json new file mode 100644 index 0000000..8d7f6e5 --- /dev/null +++ b/Backup/data/cfg/anim-profiles.json @@ -0,0 +1,499 @@ +{ + "countdown":{ + "min": 10, + "max": 75, + "hold": 700, + "ramp": 650 + }, + "profile-index": 0, + "profiles":[ + { + "name": "Profile test", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 1, + "colmain": "#000020", + "colbase": "#000020", + "density": 25, + "speed": 50 + }, + { + "anim": 2, + "colmain": "#000020", + "colbase": "#000020", + "density": 35, + "speed": 65 + }, + { + "anim": 3, + "colmain": "#000020", + "colbase": "#000020", + "density": 45, + "speed": 75 + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 4, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 70 + } + ] + }, + { + "name": "Profile 1", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 2", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 3", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 4", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 5", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 6", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + } + ] + }, + { + "name": "Profile 7", + "events":[ + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 0, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 10 + }, + { + "anim": 1, + "colmain": "#000020", + "colbase": "#000020", + "density": 5, + "speed": 70 + } + ] + } + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/anim-props.json b/Backup/data/cfg/anim-props.json new file mode 100644 index 0000000..3889b0e --- /dev/null +++ b/Backup/data/cfg/anim-props.json @@ -0,0 +1,97 @@ +{ + "twinkle":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Strobe":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Solid":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "pwr": 255 + }, + "Fade":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "HueSwirl":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Meteors":{ + "col1": "#000000", + "col2": "#000000", + "range": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Dashes":{ + "col1": "#000000", + "col2": "#000000", + "col3": "#000000", + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "DashesRange":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Fire":{ + "col1": "#000000", + "cool": 25, + "spark": 25, + "speed": 25, + "pwr": 255 + }, + "Rain":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Stacking":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Snake":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Theater":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "step": 20, + "pwr": 255 + } +} \ No newline at end of file diff --git a/Backup/data/cfg/app-events.json b/Backup/data/cfg/app-events.json new file mode 100644 index 0000000..ecf4922 --- /dev/null +++ b/Backup/data/cfg/app-events.json @@ -0,0 +1,70 @@ +{ + "index": 0, + "apps":[ + { + "name": "DSLR Booth Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "" + ] + }, + { + "name": "FotoFliqs Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop" + ] + }, + { + "name": "TouchPics Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop" + ] + }, + { + "name": "FotoZap Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "" + ] + }, + { + "name": "Twineit Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "" + ] + } + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/ble.json b/Backup/data/cfg/ble.json new file mode 100644 index 0000000..b1af60b --- /dev/null +++ b/Backup/data/cfg/ble.json @@ -0,0 +1,9 @@ +{ + "en": "true", + "core": 1, + "device-name": "ATA_COMM", + "key": "123456", + "service_uuid": "6E400001-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-rx": "6E400002-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-tx": "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" +} \ No newline at end of file diff --git a/Backup/data/cfg/buzzer.json b/Backup/data/cfg/buzzer.json new file mode 100644 index 0000000..48c7c2d --- /dev/null +++ b/Backup/data/cfg/buzzer.json @@ -0,0 +1,81 @@ +{ + "en":true, + "boot": + { + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "restart": + { + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "wifi-conn": + { + "cycles": 1, + "pause": 0, + "tune": "WifiConnected:d=16,o=5,b=112:32p,f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6" + }, + "wifi-disc": + { + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "ble-conn": + { + "cycles": 1, + "pause":0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "ble-disc": + { + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "click": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "error": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "success": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "download": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "waiting": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "beep": + { + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "test": + { + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:32p,f,g,a,b,b#" + } +} \ No newline at end of file diff --git a/Backup/data/cfg/firmware.json b/Backup/data/cfg/firmware.json new file mode 100644 index 0000000..d691b5d --- /dev/null +++ b/Backup/data/cfg/firmware.json @@ -0,0 +1,8 @@ +{ + "firm-index": 0, + "firm-locations":[ + "http://mylocation1", + "http://mylocation2", + "http://mylocation3" + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/led-devices.json b/Backup/data/cfg/led-devices.json new file mode 100644 index 0000000..f4caab1 --- /dev/null +++ b/Backup/data/cfg/led-devices.json @@ -0,0 +1,39 @@ +{ + "strip1": { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "gbr", + "shift":0, + "offset": 0, + "bright": 200, + "pin": 3, + "i2s-ch": 0, + "core": 0 + }, + "strip2": { + "en": false, + "size": 20, + "chip": "SK6812", + "rgb-order": "grb", + "shift":0, + "offset": 0, + "bright": 200, + "pin": 46, + "i2s-ch": 1, + "core": 0 + }, + "front-light": { + "en": false, + "relay": 0, + "min": 20, + "max": 100, + "core": 1 + }, + "rear-light": { + "en": false, + "relay": 1, + "min": 20, + "max": 100 + } +} \ No newline at end of file diff --git a/Backup/data/cfg/relays.json b/Backup/data/cfg/relays.json new file mode 100644 index 0000000..73ee664 --- /dev/null +++ b/Backup/data/cfg/relays.json @@ -0,0 +1,41 @@ +{ +"relays": + [ + { + "pin": 45, + "freq": 500, + "min": 0, + "max": 100, + "default": 5, + "vision": false, + "deltarate": 0 + }, + { + "pin": 48, + "freq": 500, + "min": 0, + "max": 100, + "default": 10, + "vision": false, + "deltarate": 0 + }, + { + "pin": 47, + "freq": 500, + "min": 0, + "max": 100, + "default": 20, + "vision": true, + "deltarate": 0 + }, + { + "pin": 21, + "freq": 500, + "min": 0, + "max": 100, + "default": 40, + "vision": true, + "deltarate": 0 + } + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/system.json b/Backup/data/cfg/system.json new file mode 100644 index 0000000..8cc367d --- /dev/null +++ b/Backup/data/cfg/system.json @@ -0,0 +1,37 @@ +{ + "buttons": [ + { + "en": true, + "pin": 8 + }, + { + "en": true, + "pin": 19 + }, + { + "en": true, + "pin": 0 + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "fan": { + "en": false, + "relay": 3 + }, + "t-sensor": { + "en": false, + "addr": 72, + "sp1": 85, + "fan-pwr1": 50, + "sp2": 90, + "fan-pwr2": 90, + "hyst": 1 + }, + "adc": { + "ain1_factor": 1.0 + } +} diff --git a/Backup/data/cfg/touch-pins.json b/Backup/data/cfg/touch-pins.json new file mode 100644 index 0000000..d556439 --- /dev/null +++ b/Backup/data/cfg/touch-pins.json @@ -0,0 +1,30 @@ +{ + "touchpins": + [ + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + } + ] +} \ No newline at end of file diff --git a/Backup/data/cfg/trx433.json b/Backup/data/cfg/trx433.json new file mode 100644 index 0000000..6f9cac0 --- /dev/null +++ b/Backup/data/cfg/trx433.json @@ -0,0 +1,10 @@ +{ +"rx433": { + "en": "true", + "pin": 38 + }, + "tx433": { + "en": "true", + "pin": 16 + } +} \ No newline at end of file diff --git a/Backup/data/cfg/wifi.json b/Backup/data/cfg/wifi.json new file mode 100644 index 0000000..e5b01b9 --- /dev/null +++ b/Backup/data/cfg/wifi.json @@ -0,0 +1,20 @@ +{ + "wifi": + { + "en": true, + "ap-ssid": "ATA-AP", + "ap-pass": "123456", + "mdns-name": "atadev", + "host-name": "ATADeviceXX" + }, + "cred1": + { + "ssid": "DPWifi", + "pass": "dave3.14159" + }, + "cred2": + { + "ssid": "Default", + "pass": "password" + } +} \ No newline at end of file diff --git a/Backup/data/favicon.ico b/Backup/data/favicon.ico new file mode 100644 index 0000000..3030261 Binary files /dev/null and b/Backup/data/favicon.ico differ diff --git a/Backup/data/img/atalogo.png b/Backup/data/img/atalogo.png new file mode 100644 index 0000000..40f3ee6 Binary files /dev/null and b/Backup/data/img/atalogo.png differ diff --git a/Backup/data/img/favicon-32x32.png b/Backup/data/img/favicon-32x32.png new file mode 100644 index 0000000..5118fd1 Binary files /dev/null and b/Backup/data/img/favicon-32x32.png differ diff --git a/Backup/data/www/appcontrol.html b/Backup/data/www/appcontrol.html new file mode 100644 index 0000000..94b3533 --- /dev/null +++ b/Backup/data/www/appcontrol.html @@ -0,0 +1,617 @@ +{{NAVBAR}} + + + + App Control Configuration + + + + + + +

App Control Configuration

+ +
+ Saved Animation Profiles + + + + +
+
+ + +    + + +     + +
+
+ + +
+ + +
+ Countdown Animation ( White Fill ) + + + + + + + + + + + + + + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+ Event0 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event1 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event2 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event3 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event4 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event5 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event6 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event7 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ + + + diff --git a/Backup/data/www/bleconfig.html b/Backup/data/www/bleconfig.html new file mode 100644 index 0000000..fac456f --- /dev/null +++ b/Backup/data/www/bleconfig.html @@ -0,0 +1,16 @@ +{{NAVBAR}} + + + + BLE Config + + + + + + +

App Control - Bluetooth Configuration

+ + \ No newline at end of file diff --git a/Backup/data/www/boothconfig.html b/Backup/data/www/boothconfig.html new file mode 100644 index 0000000..90427a1 --- /dev/null +++ b/Backup/data/www/boothconfig.html @@ -0,0 +1,11 @@ +{{NAVBAR}} + + + + + Photobooth Configuration + + +

Photobooth Configuration

+ + \ No newline at end of file diff --git a/Backup/data/www/edit.html b/Backup/data/www/edit.html new file mode 100644 index 0000000..f21f454 --- /dev/null +++ b/Backup/data/www/edit.html @@ -0,0 +1,144 @@ +{{NAVBAR}} + + + + Edit file + + + + + +

Edit file

+
+ Editing file: {{SAVE_PATH_INPUT}} +
+
+
+ +
+
+ {{SAVE_PATH_INPUT}} + + + + +
+
+
+ + + + + + \ No newline at end of file diff --git a/Backup/data/www/failed.html b/Backup/data/www/failed.html new file mode 100644 index 0000000..8850b8d --- /dev/null +++ b/Backup/data/www/failed.html @@ -0,0 +1,24 @@ +{{NAVBAR}} + + + + Update Failed + + + + + +
+

The update has failed.

+
+ +
+ + \ No newline at end of file diff --git a/Backup/data/www/filemanager.html b/Backup/data/www/filemanager.html new file mode 100644 index 0000000..8c96201 --- /dev/null +++ b/Backup/data/www/filemanager.html @@ -0,0 +1,297 @@ +{{NAVBAR}} + + + + File Manager + + + + + + + +

File Manager

+ +
+ File list +
+

  Total storage: {{FS_TOTAL_BYTES}}, Used: {{FS_USED_BYTES}}, Still available: {{FS_FREE_BYTES}}

+
+ + {{LISTED_FILES}} +
Listing: Size:
+
+
+ +
+ +
+ File upload +
+
+ + + +
+ +    + +
+
+
+ +
+ +
+ Edit file +
+
+ + + +
+
+
+
+ +
+ +
+ Delete file +
+
+ + + +
+
+
+
+ +
+ + + +

Firmware/File System Update

+ +
+ Firmware Update (Local) | Firmware Ver: {{FIRM_VER}} | fwataVxxx.bin or lfsataVxxx.bin +
+
+ + + + + +
+ + + +
+ +
+ + +
+ + + +
+
+
+ +
+ + +
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/Backup/data/www/global-style.css b/Backup/data/www/global-style.css new file mode 100644 index 0000000..fa8ada7 --- /dev/null +++ b/Backup/data/www/global-style.css @@ -0,0 +1,74 @@ +body { + /*background-color: #f7f7f7;*/ + font-family: Tahoma, Arial, sans-serif; + font-size: small; + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} +h1 { + margin-top: 0; +} +input{ + cursor:pointer; +} +input[type="number"]{ + width: 100px; +} +#submit { + width:120px; +} +select{ + width:180px; +} +#select-path { + width:250px; +} +#select-dir { + width:90px; +} +#spacer-50 { + height: 50px; +} +#spacer-20 { + height: 20px; +} +#spacer-10 { + height: 10px; +} +table { + /*background-color: #dddddd;*/ + border-collapse: collapse; + width:640px; + margin: 0 auto; + overflow: visible; +} +td, th { + /*border: 1px solid #dddddd;*/ + text-align: left; + padding: 2px; +} +#first_td_th { + width:400px; +} +fieldset { + width:700px; + /*background-color: #f7f7f7;*/ + border-radius: 10px; + border-color: blue; +} +#format-notice { + color: #ff0000; +} +legend { + display: flex; + justify-content: center; + background-color:white; + background-blend-mode: darken; + border-radius: 10px; + padding: 1px 8px 2px 8px; + border-style: solid; + border-width: 1.0; +} \ No newline at end of file diff --git a/Backup/data/www/home.html b/Backup/data/www/home.html new file mode 100644 index 0000000..1b6e7b2 --- /dev/null +++ b/Backup/data/www/home.html @@ -0,0 +1,185 @@ +{{NAVBAR}} + + + + + + + Welcome + + + + +

ATA Booth Summary

+ +
+ Booth Overview +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Mode: Lumia + + LED Strip1: Active + + Front Light: Active +
+
+
+ App: DSLRBooth + + LED Strip2: Active + + Rear Light: Active +
+
+
+ ... + + ... + + OLED: Active +
+
+
+ +
+ +
+ System Info: +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Firmware Ver: 1.0 + + Flash Size: + + RAM Size: +
+
+
+ Booth T°: 80.3F + + Flash Free: + + Used RAM: +
+
+
+ Setpoint T°: 85F + + ... + + Free RAM: +
+
+
+ +
+ +
+ Network / WiFi +
+ + + + + + + + + + + + + + +
+ IP Addr: 192.XXX.XXX.XXX + + MAC Addr: + + WiFi SSID: +
+
+
+ WiFi RSSi: + + WiFi Ch: + + Wifi Encryp: +
+
+
+ +
+ +
+ Bluetooth LE +
+ + + + + + + +
+ Active: true + + Connected: true + + Name: ATADevXX +
+
+
+ + \ No newline at end of file diff --git a/Backup/data/www/info.html b/Backup/data/www/info.html new file mode 100644 index 0000000..5ddcf4c --- /dev/null +++ b/Backup/data/www/info.html @@ -0,0 +1,11 @@ +{{NAVBAR}} + + + + + Welcome + + +

System Information

+ + \ No newline at end of file diff --git a/Backup/data/www/navbar.html b/Backup/data/www/navbar.html new file mode 100644 index 0000000..460c215 --- /dev/null +++ b/Backup/data/www/navbar.html @@ -0,0 +1,56 @@ + + + + + + +
+ +
+ + \ No newline at end of file diff --git a/Backup/data/www/ok.html b/Backup/data/www/ok.html new file mode 100644 index 0000000..11be6e0 --- /dev/null +++ b/Backup/data/www/ok.html @@ -0,0 +1,24 @@ +{{NAVBAR}} + + + + Update Success + + + + + +
+

The update was successful.

+
+ +
+ + \ No newline at end of file diff --git a/Backup/data/www/wifiportal.html b/Backup/data/www/wifiportal.html new file mode 100644 index 0000000..f6bf4d4 --- /dev/null +++ b/Backup/data/www/wifiportal.html @@ -0,0 +1,128 @@ +{{NAVBAR}} + + + + + WiFi Credentials + + + +

Enter WiFi Credentials

+
+ + + + + + Image not found +
+ + + + diff --git a/ToDo.txt b/ToDo.txt new file mode 100644 index 0000000..16628d5 --- /dev/null +++ b/ToDo.txt @@ -0,0 +1,15 @@ +1 - Second Strip Animation + a - How to synchronize the 2 Animation + b - Set Default directions? + +2 - Wifi Server + +3 - OTA Upgrade + +4 - menu + a - file manager & editor + b - wifi credentials + c - firmware update + d - mode selection page + i - booth config and reboot + ii - \ No newline at end of file diff --git a/boards/wroom-32S3-N8R2.json b/boards/wroom-32S3-N8R2.json new file mode 100644 index 0000000..9fd8b41 --- /dev/null +++ b/boards/wroom-32S3-N8R2.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DBOARD_HAS_PSRAM" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "My ESP32-S3-DevKitN8R2 (8 MB QD, 2M PSRAM)", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 524288, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitc-1.html", + "vendor": "Espressif" +} diff --git a/boards/wroom-32d-8mb.json b/boards/wroom-32d-8mb.json new file mode 100644 index 0000000..b39c73d --- /dev/null +++ b/boards/wroom-32d-8mb.json @@ -0,0 +1,37 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32_out.ld" + }, + "core": "esp32", + "extra_flags": "-DARDUINO_ESP32_DEV", + "f_cpu": "240000000L", + "f_flash": "40000000L", + "flash_mode": "dio", + "mcu": "esp32", + "variant": "esp32" + }, + "connectivity": [ + "wifi", + "bluetooth", + "ethernet", + "can" + ], + "debug": { + "openocd_board": "esp-wroom-32.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "WROOM-32D DEV MODULE", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://en.wikipedia.org/wiki/ESP32", + "vendor": "Consius" + } \ No newline at end of file diff --git a/data/ata-boothifier-upgrade.html b/data/ata-boothifier-upgrade.html new file mode 100644 index 0000000..3327522 --- /dev/null +++ b/data/ata-boothifier-upgrade.html @@ -0,0 +1,508 @@ + + + + + + ATA Firmware Update + + + + +

ATA Firmware Update

+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ +
+ + + + diff --git a/data/boards/board14.json b/data/boards/board14.json new file mode 100644 index 0000000..025edf3 --- /dev/null +++ b/data/boards/board14.json @@ -0,0 +1,30 @@ +{ + "rgb1": 3, + "rgb2": 46, + "btn1": 8, + "btn2": 19, + "btn3": 0, + "buzzer": 37, + "touch1": 9, + "touch2": 10, + "touch3": 11, + "touch4": 12, + "touch5": 13, + "shield": 9, + "relay1": 1, + "relay2": 2, + "relay3": 3, + "relay4": 4, + "stat1": 17, + "stat2": 18, + "adc1": 20, + "oled_dc": 7, + "oled_rst": 6, + "oled_mosi": 5, + "oled_sck": 4, + "oled_cs": 15, + "ext1": 35, + "ext2": 36, + "rf433tx": 38, + "rf433rx": 16 +} \ No newline at end of file diff --git a/data/boards/board15.json b/data/boards/board15.json new file mode 100644 index 0000000..c092deb --- /dev/null +++ b/data/boards/board15.json @@ -0,0 +1,30 @@ +{ + "rgb1": 3, + "rgb2": 9, + "btn1": 8, + "btn2": 18, + "btn3": 0, + "buzzer": 42, + "touch1": 11, + "touch2": 12, + "touch3": 13, + "touch4": 21, + "touch5": -1, + "shield": 14, + "relay1": 38, + "relay2": 48, + "relay3": 47, + "relay4": 21, + "stat1": 39, + "stat2": 37, + "adc1": 10, + "oled_dc": 7, + "oled_rst": 6, + "oled_mosi": 5, + "oled_sck": 4, + "oled_cs": 15, + "ext1": 41, + "ext2": -1, + "rf433tx": 40, + "rf433rx": 16 +} \ No newline at end of file diff --git a/data/boards/test.json b/data/boards/test.json new file mode 100644 index 0000000..97bfaa6 --- /dev/null +++ b/data/boards/test.json @@ -0,0 +1,30 @@ +{ + "rgb1": 3, + "rgb2": 38, + "btn1": 8, + "btn2": 19, + "btn3": -1, + "buzzer": -1, + "touch1": -1, + "touch2": -1, + "touch3": -1, + "touch4": -1, + "touch5": -1, + "shield": -1, + "relay1": 1, + "relay2": 2, + "relay3": 3, + "relay4": 4, + "stat1": -1, + "stat2": -1, + "adc1": 10, + "oled_dc": 7, + "oled_rst": 6, + "oled_mosi": 5, + "oled_sck": 4, + "oled_cs": 15, + "ext1": -1, + "ext2": -1, + "rf433tx": -1, + "rf433rx": -1 +} \ No newline at end of file diff --git a/data/booths/custom.json b/data/booths/custom.json new file mode 100644 index 0000000..e01c81a --- /dev/null +++ b/data/booths/custom.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "WS6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "WS6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/helio-flare.json b/data/booths/helio-flare.json new file mode 100644 index 0000000..f40c0e9 --- /dev/null +++ b/data/booths/helio-flare.json @@ -0,0 +1,137 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 168, + "chip": "SK6812", + "rgb-order": "RGB", + "shift":-42, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + }, + { + "en": true, + "size": 20, + "chip": "SK6812", + "rgb-order": "RGB", + "shift":0, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/helio-posh.json b/data/booths/helio-posh.json new file mode 100644 index 0000000..396c345 --- /dev/null +++ b/data/booths/helio-posh.json @@ -0,0 +1,134 @@ +{ + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/helio-sport.json b/data/booths/helio-sport.json new file mode 100644 index 0000000..46efdbe --- /dev/null +++ b/data/booths/helio-sport.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/light-stik.json b/data/booths/light-stik.json new file mode 100644 index 0000000..824685e --- /dev/null +++ b/data/booths/light-stik.json @@ -0,0 +1,138 @@ +{ + "mode": "stik", + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], + "pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 144, + "chip": "SK6812", + "rgb-order": "RGB", + "shift":0, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + }, + { + "en": false, + "size": 20, + "chip": "SK6812", + "rgb-order": "RGB", + "shift":0, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "FlashStik", + "core": 1 + } + } + \ No newline at end of file diff --git a/data/booths/lumia-m.json b/data/booths/lumia-m.json new file mode 100644 index 0000000..2f66e2c --- /dev/null +++ b/data/booths/lumia-m.json @@ -0,0 +1,140 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "buzzer":{ + "en": true + }, + "strips": + [ + { + "en": true, + "size": 168, + "chip": "SK6812", + "rgb-order": "RGB", + "shift": 0, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "RGB", + "shift": 0, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1, + "bright": 200 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/lumia-spectra.json b/data/booths/lumia-spectra.json new file mode 100644 index 0000000..7a39950 --- /dev/null +++ b/data/booths/lumia-spectra.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/lumia-xl.json b/data/booths/lumia-xl.json new file mode 100644 index 0000000..3a814b3 --- /dev/null +++ b/data/booths/lumia-xl.json @@ -0,0 +1,159 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + }, + "wifi-client":{ + "en": true, + "mdns-name": "atadev", + "ssid": "padillaguest", + "pass": "padillaguest" + }, + "wifi-ap":{ + "ssid": "ATA_AP", + "append-id": true, + "pass": "12345678", + "ip": [192,168,10.1], + "gateway": [192,168,10,200], + "subnet": [255,255,255,0] + }, + "animation1":{ + "white-max-time": 30, + "white-min": 20, + "white-max": 100, + "toggle-cycles": 2, + "comet-size": 0.2, + "comet-fade": 96, + "fire-cooling": 66, + "fire-sparking": 62 + } +} diff --git a/data/booths/m1.json b/data/booths/m1.json new file mode 100644 index 0000000..7a39950 --- /dev/null +++ b/data/booths/m1.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/marquee.json b/data/booths/marquee.json new file mode 100644 index 0000000..7a39950 --- /dev/null +++ b/data/booths/marquee.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/roamer-big.json b/data/booths/roamer-big.json new file mode 100644 index 0000000..90face0 --- /dev/null +++ b/data/booths/roamer-big.json @@ -0,0 +1,136 @@ +{ + "mode": "roamer", + "button": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 168, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-42, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": false, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/roamer.json b/data/booths/roamer.json new file mode 100644 index 0000000..146b248 --- /dev/null +++ b/data/booths/roamer.json @@ -0,0 +1,136 @@ +{ + "mode": "roamer", + "button": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": false, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/booths/testbooth.json b/data/booths/testbooth.json new file mode 100644 index 0000000..9f33075 --- /dev/null +++ b/data/booths/testbooth.json @@ -0,0 +1,135 @@ +{ + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": false, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + }, + { + "en": false, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5, + "vision": true + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": false, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strips": + [ + { + "en": true, + "size": 138, + "chip": "WS6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + { + "en": true, + "size": 30, + "chip": "WS6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + } + ], + "rx433": { + "en": false + }, + "tx433": { + "en": false + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + } +} diff --git a/data/css/global-style.css b/data/css/global-style.css new file mode 100644 index 0000000..a4edce2 --- /dev/null +++ b/data/css/global-style.css @@ -0,0 +1,79 @@ +body { + /*background-color: #f7f7f7;*/ + font-family: Tahoma, Arial, sans-serif; + font-size: small; + margin: 0; + display: flex; + min-height: 100vh; +} +.main-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 20px; +} +h1 { + margin-top: 0; +} +input{ + cursor:pointer; +} +input[type="number"]{ + width: 100px; +} +#submit { + width:120px; +} +select{ + width:160px; +} +#select-path { + width:250px; +} +#select-dir { + width:90px; +} +#spacer-50 { + height: 50px; +} +#spacer-20 { + height: 20px; +} +#spacer-10 { + height: 10px; +} +table { + /*background-color: #dddddd;*/ + border-collapse: collapse; + width:600px; + margin: 0 auto; + overflow: visible; +} +td, th { + /*border: 1px solid #dddddd;*/ + text-align: left; + padding: 2px; +} +#first_td_th { + width:400px; +} +fieldset { + width:620px; + /*background-color: #f7f7f7;*/ + border-radius: 10px; + border-color: blue; +} +#format-notice { + color: #ff0000; +} +legend { + display: flex; + justify-content: center; + background-color:white; + background-blend-mode: darken; + border-radius: 10px; + padding: 1px 8px 2px 8px; + border-style: solid; + border-width: 1.0; +} \ No newline at end of file diff --git a/data/css/nav.css b/data/css/nav.css new file mode 100644 index 0000000..f74f175 --- /dev/null +++ b/data/css/nav.css @@ -0,0 +1,72 @@ +.navbar { + width: 100%; + background-color: black; + border-bottom: 2px solid white; + display: flex; + justify-content: flex-start; + align-items: center; + position: sticky; + top: 0; + z-index: 1000; + } + .navbar-left { + padding: 0 10px; + } + .navbar ul { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + position: relative; /* Ensure relative positioning for the submenu */ + } + .navbar ul li { + float: left; + position: relative; /* Ensure relative positioning for the submenu */ + } + .navbar ul li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; + border-right: 1px solid white; + } + .navbar ul li a:hover { + background-color: grey; + } + .navbar ul li a.disabled { + color: grey; + pointer-events: none; + } + .navbar ul .submenu { + display: none; + position: absolute; + top: 100%; + left: 0; + background-color: black; + list-style-type: none; + margin: 0; + padding: 0; + border-top: 2px solid white; + z-index: 1000; + } + .navbar ul .submenu li { + float: none; + border-right: none; + } + .navbar ul .submenu li a { + padding: 10px 16px; + border-bottom: 1px solid white; + } + .navbar ul .submenu li a:hover { + background-color: grey; + } + .navbar ul li:hover > .submenu { + display: block; + } + .nav-image { + height: 40px; + width: auto; + } + \ No newline at end of file diff --git a/data/favicon.ico b/data/favicon.ico new file mode 100644 index 0000000..3030261 Binary files /dev/null and b/data/favicon.ico differ diff --git a/data/flashstik-reg.html b/data/flashstik-reg.html new file mode 100644 index 0000000..7cef51f --- /dev/null +++ b/data/flashstik-reg.html @@ -0,0 +1,472 @@ + + + + + + ATA Light Stick Reg + + + + +

ATA Flash-Stick Link/Registration

+ + +
+ + +
+ +
+ + +
+ +
+ + + +
+ + + + + + + diff --git a/data/images/atalogo.png b/data/images/atalogo.png new file mode 100644 index 0000000..40f3ee6 Binary files /dev/null and b/data/images/atalogo.png differ diff --git a/data/images/favicon-32x32.png b/data/images/favicon-32x32.png new file mode 100644 index 0000000..5118fd1 Binary files /dev/null and b/data/images/favicon-32x32.png differ diff --git a/data/js/event-box.js b/data/js/event-box.js new file mode 100644 index 0000000..cb01b1b --- /dev/null +++ b/data/js/event-box.js @@ -0,0 +1,442 @@ +class EventBox extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + +
+
+ Event0
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ `; + this.Title = this.shadowRoot.querySelector('#center-text'); + this.hueSelect = this.shadowRoot.querySelector('#hue-selector'); + this.AnimationList = this.shadowRoot.getElementById('animation-list'); + this.Speed = this.shadowRoot.getElementById('speed'); + this.HueRange = this.shadowRoot.querySelector('#huerange'); + this.Param1 = this.shadowRoot.querySelector('#param1'); + this.Param2 = this.shadowRoot.querySelector('#param2'); + this.Check1 = this.shadowRoot.querySelector('#check1'); + this.Check2 = this.shadowRoot.querySelector('#check2'); + this.Check3 = this.shadowRoot.querySelector('#check3'); + this.Check4 = this.shadowRoot.querySelector('#check4'); + + this.AnimationListLabel = this.shadowRoot.querySelector('#list-label'); + this.SpeedLabel = this.shadowRoot.querySelector('#speed-label'); + this.HueRangeLabel = this.shadowRoot.querySelector('#huerange-label'); + this.Param1Label = this.shadowRoot.getElementById('param1-label'); + this.Param2Label = this.shadowRoot.getElementById('param2-label'); + this.Check1Label = this.shadowRoot.getElementById('check1-label'); + this.Check2Label = this.shadowRoot.getElementById('check2-label'); + this.Check3Label = this.shadowRoot.getElementById('check3-label'); + this.Check4Label = this.shadowRoot.getElementById('check4-label'); + + this.AnimationPropsJson; + + this.SpeedCaption = ""; + this.HueRangeCaption = ""; + this.Param1Caption = ""; + this.Param2Caption = ""; + this.Check1Caption = ""; + this.Check2Caption = ""; + this.Check3Caption = ""; + + this.setSpeedCaption(`Speed: `); + this.Speed.min = 0; + this.Speed.max = 100; + + this.setHueRangeCaption(`Hue Range: `); + this.HueRange.min = 0; + this.HueRange.max = 360; + + this.setParam1Caption(`Param1: `); + this.Param1.min = 0; + this.Param1.max = 100; + + this.setParam2Caption(`Param2: `); + this.Param2.min = 0; + this.Param2.max = 100; + + this.setCheck1Caption(`Check1`); + this.setCheck2Caption(`Check2`); + this.setCheck3Caption(`Check3`); + this.setCheck4Caption(`50% Lum`); + + this.Index = 0; + + this.Speed.addEventListener('input', () => { this.updateSpeedLabel(); }); + this.HueRange.addEventListener('input', () => { this.updateHueRangeLabel(); }); + this.Param1.addEventListener('input', () => { this.updateParam1Label(); }); + this.Param2.addEventListener('input', () => { this.updateParam2Label(); }); + + this.TryButton = this.shadowRoot.querySelector('#try-button'); + this.TryButton.addEventListener('click', () => { + this.handleTryButtonClick(); + }); + + this.AnimationList.addEventListener('change', this.handleAnimationListChange.bind(this)); + } + + setIndex(i){ this.Index = i; } + getIndex(){ return this.Index; } + updateSpeedLabel(){ + if(!this.Speed.hidden){ + this.SpeedLabel.innerHTML = this.SpeedCaption + this.getSpeedValue(); + } + } + updateHueRangeLabel(){ + if(!this.HueRange.hidden){ + this.HueRangeLabel.innerHTML = this.HueRangeCaption + this.getHueRangeValue(); + } + } + updateParam1Label(){ + if(!this.Param1.hidden){ + this.Param1Label.innerHTML = this.Param1Caption + this.getParam1Value(); + } + } + updateParam2Label(){ + if(!this.Param2.hidden){ + this.Param2Label.innerHTML = this.Param2Caption + this.getParam2Value(); + } + } + setTitle(text){ + this.Title.textContent = text; + } + addOptionToList(value, text){ + const optionElement = document.createElement('option'); + optionElement.value = value; + optionElement.textContent = text; + this.AnimationList.appendChild(optionElement); + } + setHidden(hidden){ this.hidden = hidden; } + getHidden(){ return this.hidden; } + setHueValue(value){ this.hueSelect.setHue(value); } + getHueValue(){ return this.hueSelect.getSelectedHue(); } + setSpeedValue(value){ + this.Speed.value = value; + this.updateSpeedLabel(); + } + setSpeedCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.SpeedCaption = caption; + this.SpeedLabel.innerHTML = caption; + this.Speed.hidden = hid; + this.SpeedLabel.hidden = hid; + } + getSpeedValue(){ return this.Speed.value; } + setHueRangeValue(value){ + this.HueRange.value = value; + this.updateHueRangeLabel() + } + setHueRangeCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.HueRangeCaption = caption; + this.HueRangeLabel.innerHTML = caption; + this.HueRange.hidden = hid; + this.HueRangeLabel.hidden = hid; + } + getHueRangeValue(){ return this.HueRange.value; } + setParam1Value(value){ + this.Param1.value = value; + this.updateParam1Label() + } + setParam1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param1Caption = caption; + this.Param1Label.innerHTML = caption; + this.Param1.hidden = hid; + this.Param1Label.hidden = hid; + } + getParam1Value(){ return this.Param1.value; } + setParam2Value(value){ + this.Param2.value = value; + this.updateParam2Label() + } + setParam2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param2Caption = caption; + this.Param2Label.innerHTML = caption; + this.Param2.hidden = hid; + this.Param2Label.hidden = hid; + } + getParam2Value(){ return this.Param2.value; } + + setCheck1Value(value){ this.Check1.checked = value; } + setCheck1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check1Label.innerHTML = caption; + this.Check1.hidden = hid; + this.Check1Label.hidden = hid; + } + getCheck1Value(){ return this.Check1.checked; } + + setCheck2Value(value){ this.Check2.checked = value; } + setCheck2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check2Label.innerHTML = caption; + this.Check2.hidden = hid; + this.Check2Label.hidden = hid; + } + getCheck2Value(){ return this.Check2.checked; } + + setCheck3Value(value){ this.Check3.checked = value; } + setCheck3Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check3Label.innerHTML = caption; + this.Check3.hidden = hid; + this.Check3Label.hidden = hid; + } + getCheck3Value(){ return this.Check3.checked; } + + setCheck4Value(value){ this.Check4.checked = value; } + setCheck4Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check4Label.innerHTML = caption; + this.Check4.hidden = hid; + this.Check4Label.hidden = hid; + } + getCheck4Value(){ return this.Check4.checked; } + + setAnimationIndex(index){ + this.AnimationList.selectedIndex = index; + const changeEvent = new Event('change'); + this.AnimationList.dispatchEvent(changeEvent); + } + getAnimationIndex(){ + return this.AnimationList.selectedIndex; + } + + handleTryButtonClick() { + const eventIndexValue = this.getIndex(); + const animIndexValue = this.getAnimationIndex(); + const hueValue = this.getHueValue(); + const speedValue = this.getSpeedValue(); + const colorRangeValue = this.getHueRangeValue(); + const param1Value = this.getParam1Value(); + const param2Value = this.getParam2Value(); + const check1Value = this.getCheck1Value(); + const check2Value = this.getCheck2Value(); + const check3Value = this.getCheck3Value(); + const check4Value = this.getCheck4Value(); + + this.dispatchEvent(new CustomEvent('tryClick', { + detail: { + eventIndex: eventIndexValue, + animIndex: animIndexValue, + hue: hueValue, + speed: speedValue, + colorRange: colorRangeValue, + param1: param1Value, + param2: param2Value, + check1: check1Value, + check2: check2Value, + check3: check3Value, + check4: check4Value + } + })); + } + + setAnimationCaptions(propsJson){ + this.AnimationPropsJson = propsJson; + + //add options to list + let x = 0; + this.AnimationPropsJson.forEach(props => { + this.addOptionToList(x, props.name); + x++; + }); + + } + + handleAnimationListChange(){ + const selectedIndex = this.getAnimationIndex(); + const selectedProps = this.AnimationPropsJson[selectedIndex]; + + this.updateControlProps(selectedProps); + } + + updateControlProps(props){ + this.setSpeedCaption(props.speed); + let s = props['hue-range']; + this.setHueRangeCaption(s || ""); + this.setParam1Caption(props.param1 || ""); + this.setParam2Caption(props.param2 || ""); + this.setCheck1Caption(props.check1 || ""); + this.setCheck2Caption(props.check2 || ""); + this.setCheck3Caption(props.check3 || ""); + this.setCheck4Caption(props.check4 || ""); + + this.updateSpeedLabel(); + this.updateHueRangeLabel(); + this.updateParam1Label(); + this.updateParam2Label(); + } + +} + +customElements.define('event-box', EventBox); + \ No newline at end of file diff --git a/data/js/fwUoload.js b/data/js/fwUoload.js new file mode 100644 index 0000000..446f3b4 --- /dev/null +++ b/data/js/fwUoload.js @@ -0,0 +1,64 @@ +async function uploadFile(event) { + event.preventDefault(); + + const file = fileInput.files[0]; + if (!file) { + alert("Please select a file."); + return; + } + + // Existing file validation code... + + const formData = new FormData(); + formData.append('file', file); + + // Create a custom reader function + const reader = file.stream().getReader(); + const totalLength = file.size; + + let uploaded = 0; + + // Create a readable stream and use the custom reader function + const uploadStream = new ReadableStream({ + async start(controller) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + uploaded += value.length; + let percentUploaded = (uploaded / totalLength * 100).toFixed(2); + + // Update progress bar and label + progressBar.value = percentUploaded; + progressLabel.innerHTML = `${percentUploaded}%`; + + controller.enqueue(value); + } + controller.close(); + }, + }); + + // Create a new Request with the readable stream as body + const req = new Request('/update', { + method: 'POST', + body: uploadStream, + headers: { + // Add any relevant headers here + }, + }); + + try { + const response = await fetch(req); + + if (response.ok) { + alert('Upload completed!'); + progressLabel.innerHTML = "Completed!"; + } else { + alert('An error occurred during the upload.'); + progressLabel.innerHTML = "Error!"; + } + } catch (error) { + console.error('Upload failed:', error); + } + } + \ No newline at end of file diff --git a/data/js/hue-select.js b/data/js/hue-select.js new file mode 100644 index 0000000..ca067d1 --- /dev/null +++ b/data/js/hue-select.js @@ -0,0 +1,244 @@ + +class HueSelect extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + + `; + + this.currentHue = 0; + this.colorList; + this.hueLabel = this.shadowRoot.getElementById('hue-label'); + + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + selectedColorDiv.addEventListener('click', () => { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = dropdownContent.style.display === 'block' ? 'none' : 'block'; + }); + + this.createColorOptions(); + this.setHue(0); + } + + generateColors() { + const colors = []; + for (let hue = 0; hue <= 360; hue += 10) { + if (hue == 360) { hue = 359; } + colors.push(hue); + } + + colors.push(-1); + colors.push(-2); + return colors; + } + + createColorOption(hue) { + const colorOption = document.createElement('div'); + colorOption.classList.add('color-option'); + + const colorPatch = document.createElement('span'); + colorPatch.classList.add('color-patch'); + + const colorText = document.createElement('span'); + colorText.classList.add('color-text'); + colorText.textContent = hue; + + const rgbHex = document.createElement('span'); + rgbHex.classList.add('rgb-hex'); + + if (hue === -2) { + colorPatch.style.backgroundColor = 'rgb(0,0,0)'; + rgbHex.innerHTML = '  #000000'; + } else if (hue === -1) { + colorPatch.style.backgroundColor = 'rgb(255,255,255)'; + rgbHex.innerHTML = '  #FFFFFF'; + } else { + colorPatch.style.backgroundColor = `hsl(${hue}, 100%, 50%)`; + const hexColor = this.hslToRgb(hue, 100, 50).toUpperCase(); + rgbHex.innerHTML = `  ${hexColor}`; + } + + colorOption.appendChild(colorPatch); + colorOption.appendChild(colorText); + colorOption.appendChild(rgbHex); + + colorOption.addEventListener('click', () => this.handleColorSelection(hue)); + + return colorOption; + } + + createColorOptions() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + this.colorList = this.generateColors(); + + this.colorList.forEach(hue => { + const colorOption = this.createColorOption(hue); + dropdownContent.appendChild(colorOption); + }); + } + + // Function to convert HSL to RGB + hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + let r, g, b; + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = (x) => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + handleColorSelection(hue) { + this.currentHue = hue; + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + // Update the color patch and hue label + const rgb = this.getRGBfromHue(hue); + colorPatch.style.backgroundColor = rgb; + this.setHueLabel(hue); + this.hideDropdown(); + + this.dispatchEvent(new CustomEvent('change', { detail: { hue, rgb } })); + } + + // Method to get the hue value of the selected item + getSelectedHue() { + return this.currentHue; + } + + getSelectedRGB() { + const selectedColorText = this.shadowRoot.getElementById('selectedColor').textContent.trim(); + return parseFloat(selectedColorText); + } + + getRGBfromHue(hue){ + if(hue == -1){ + return '#FFFFFF'; + }else if(hue == -2){ + return '#000000'; + }else{ + return this.hslToRgb(hue, 100, 50);; + } + } + + // Method to get the RGB value of the selected item + getSelectedRgb() { + const selectedHue = this.getSelectedHue(); + return getRGBfromHue(selectedHue); + } + + setHue(hue) { + // Round the input hue to the nearest 10th + let roundedHue = hue; + if(hue === -1 || hue === -2){ + roundedHue = hue; + }else{ + roundedHue = Math.round(hue / 10) * 10; + if (roundedHue >= 360) { + roundedHue = 359; + }else if (roundedHue < 0) { + roundedHue = 0; + } + } + + this.currentHue = roundedHue; + + this.colorList.forEach(colorVal => { + if (colorVal === roundedHue) { + this.handleColorSelection(roundedHue); + } + }); + + this.hideDropdown(); + } + + hideDropdown() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = 'none'; + } + + setHueLabel(value){ + this.hueLabel.textContent = "Hue: " + value; + } + + + } + + customElements.define('hue-select', HueSelect); + \ No newline at end of file diff --git a/data/js/jquery-3.7.1.js b/data/js/jquery-3.7.1.js new file mode 100644 index 0000000..f7d84ab --- /dev/null +++ b/data/js/jquery-3.7.1.js @@ -0,0 +1,8673 @@ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + +var document = window.document; + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} + +var version = "3.7.1", + + rhtmlSuffix = /HTML$/i, + + jQuery = function( selector, context ) { + + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + jquery: version, + + constructor: jQuery, + + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + get: function( num ) { + + if ( num == null ) { + return slice.call( this ); + } + + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + pushStack: function( elems ) { + + var ret = jQuery.merge( this.constructor(), elems ); + + ret.prevObject = this; + + return ret; + }, + + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + if ( typeof target === "boolean" ) { + deep = target; + + target = arguments[ i ] || {}; + i++; + } + + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + if ( ( options = arguments[ i ] ) != null ) { + + for ( name in options ) { + copy = options[ name ]; + + if ( name === "__proto__" || target === copy ) { + continue; + } + + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + target[ name ] = jQuery.extend( deep, clone, copy ); + + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + return target; +}; + +jQuery.extend( { + + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + if ( !proto ) { + return true; + } + + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + text: function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + while ( ( node = elem[ i++ ] ) ) { + + ret += jQuery.text( node ); + } + } + if ( nodeType === 1 || nodeType === 11 ) { + return elem.textContent; + } + if ( nodeType === 9 ) { + return elem.documentElement.textContent; + } + if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + return ret; + }, + + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + isXMLDoc: function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + return flat( ret ); + }, + + guid: 1, + + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var pop = arr.pop; + +var sort = arr.sort; + +var splice = arr.splice; + +var whitespace = "[\\x20\\t\\r\\n\\f]"; + +var rtrimCSS = new RegExp( + "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", + "g" +); + +jQuery.contains = function( a, b ) { + var bup = b && b.parentNode; + + return a === bup || !!( bup && bup.nodeType === 1 && ( + + a.contains ? + a.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); +}; + +var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + +function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + + if ( ch === "\0" ) { + return "\uFFFD"; + } + + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + return "\\" + ch; +} + +jQuery.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +var preferredDoc = document, + pushNative = push; + +( function() { + +var i, + Expr, + outermostContext, + sortInput, + hasDuplicate, + push = pushNative, + + document, + documentElement, + documentIsHTML, + rbuggyQSA, + matches, + + expando = jQuery.expando, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + + "loop|multiple|open|readonly|required|scoped", + + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + "*([*^$|!~]?=)" + whitespace + + + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + ".*" + + ")\\)|)", + + rwhitespace = new RegExp( whitespace + "+", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + + whitespace + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + + needsContext: new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + return nonHex; + } + + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && nodeName( elem, "fieldset" ); + }, + { dir: "parentNode", next: "legend" } + ); + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { + apply: function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + }, + call: function( target ) { + pushNative.apply( target, slice.call( arguments, 1 ) ); + } + }; +} + +function find( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + nodeType = context ? context.nodeType : 9; + + results = results || []; + + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + if ( ( m = match[ 1 ] ) ) { + + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + if ( elem.id === m ) { + push.call( results, elem ); + return results; + } + } else { + return results; + } + + } else { + + if ( newContext && ( elem = newContext.getElementById( m ) ) && + find.contains( context, elem ) && + elem.id === m ) { + + push.call( results, elem ); + return results; + } + } + + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + if ( !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { + + newSelector = selector; + newContext = context; + + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + if ( newContext != context || !support.scope ) { + + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); +} + +function createCache() { + var keys = []; + + function cache( key, value ) { + + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + el = null; + } +} + +function createInputPseudo( type ) { + return function( elem ) { + return nodeName( elem, "input" ) && elem.type === type; + }; +} + +function createButtonPseudo( type ) { + return function( elem ) { + return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && + elem.type === type; + }; +} + +function createDisabledPseudo( disabled ) { + + return function( elem ) { + + if ( "form" in elem ) { + + if ( elem.parentNode && elem.disabled === false ) { + + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + return elem.isDisabled === disabled || + + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + return false; + }; +} + +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +function setDocument( node ) { + var subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + document = doc; + documentElement = document.documentElement; + documentIsHTML = !jQuery.isXMLDoc( document ); + + matches = documentElement.matches || + documentElement.webkitMatchesSelector || + documentElement.msMatchesSelector; + + if ( documentElement.msMatchesSelector && + + preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + subWindow.addEventListener( "unload", unloadHandler ); + } + + support.getById = assert( function( el ) { + documentElement.appendChild( el ).id = jQuery.expando; + return !document.getElementsByName || + !document.getElementsByName( jQuery.expando ).length; + } ); + + support.disconnectedMatch = assert( function( el ) { + return matches.call( el, "*" ); + } ); + + support.scope = assert( function() { + return document.querySelectorAll( ":scope" ); + } ); + + support.cssHas = assert( function() { + try { + document.querySelector( ":has(*,:jqfake)" ); + return false; + } catch ( e ) { + return true; + } + } ); + + if ( support.getById ) { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + Expr.find.TAG = function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + } else { + return context.querySelectorAll( tag ); + } + }; + + Expr.find.CLASS = function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + rbuggyQSA = []; + + assert( function( el ) { + + var input; + + documentElement.appendChild( el ).innerHTML = + "" + + ""; + + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + documentElement.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + } ); + + if ( !support.cssHas ) { + + rbuggyQSA.push( ":has" ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + + sortOrder = function( a, b ) { + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + 1; + + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + if ( a === document || a.ownerDocument == preferredDoc && + find.contains( preferredDoc, a ) ) { + return -1; + } + + if ( b === document || b.ownerDocument == preferredDoc && + find.contains( preferredDoc, b ) ) { + return 1; + } + + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + }; + + return document; +} + +find.matches = function( expr, elements ) { + return find( expr, null, null, elements ); +}; + +find.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + if ( ret || support.disconnectedMatch || + + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return find( expr, document, null, [ elem ] ).length > 0; +}; + +find.contains = function( context, elem ) { + + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return jQuery.contains( context, elem ); +}; + +find.attr = function( elem, name ) { + + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + if ( val !== undefined ) { + return val; + } + + return elem.getAttribute( name ); +}; + +find.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +jQuery.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + hasDuplicate = !support.sortStable; + sortInput = !support.sortStable && slice.call( results, 0 ); + sort.call( results, sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + splice.call( results, duplicates[ j ], 1 ); + } + } + + sortInput = null; + + return results; +}; + +jQuery.fn.uniqueSort = function() { + return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); +}; + +Expr = jQuery.expr = { + + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + ATTR: function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) + .replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + if ( !match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + } else if ( match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + } else if ( unquoted && rpseudo.test( unquoted ) && + + ( excess = tokenize( unquoted, true ) ) && + + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + return match.slice( 0, 3 ); + } + }, + + filter: { + + TAG: function( nodeNameSelector ) { + var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return nodeName( elem, expectedNodeName ); + }; + }, + + CLASS: function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + ")" + className + + "(" + whitespace + "|$)" ) ) && + classCache( className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + ATTR: function( name, operator, check ) { + return function( elem ) { + var result = find.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + if ( operator === "=" ) { + return result === check; + } + if ( operator === "!=" ) { + return result !== check; + } + if ( operator === "^=" ) { + return check && result.indexOf( check ) === 0; + } + if ( operator === "*=" ) { + return check && result.indexOf( check ) > -1; + } + if ( operator === "$=" ) { + return check && result.slice( -check.length ) === check; + } + if ( operator === "~=" ) { + return ( " " + result.replace( rwhitespace, " " ) + " " ) + .indexOf( check ) > -1; + } + if ( operator === "|=" ) { + return result === check || result.slice( 0, check.length + 1 ) === check + "-"; + } + + return false; + }; + }, + + CHILD: function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) { + + return false; + } + } + + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + if ( forward && useCache ) { + + outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + if ( useCache ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + if ( diff === false ) { + + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) && + ++diff ) { + + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + outerCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + PSEUDO: function( pseudo, argument ) { + + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + find.error( "unsupported pseudo: " + pseudo ); + + if ( fn[ expando ] ) { + return fn( argument ); + } + + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + not: markFunction( function( selector ) { + + var input = [], + results = [], + matcher = compile( selector.replace( rtrimCSS, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + has: markFunction( function( selector ) { + return function( elem ) { + return find( selector, elem ).length > 0; + }; + } ), + + contains: markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; + }; + } ), + + lang: markFunction( function( lang ) { + + if ( !ridentifier.test( lang || "" ) ) { + find.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + target: function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + root: function( elem ) { + return elem === documentElement; + }, + + focus: function( elem ) { + return elem === safeActiveElement() && + document.hasFocus() && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + enabled: createDisabledPseudo( false ), + disabled: createDisabledPseudo( true ), + + checked: function( elem ) { + + return ( nodeName( elem, "input" ) && !!elem.checked ) || + ( nodeName( elem, "option" ) && !!elem.selected ); + }, + + selected: function( elem ) { + + if ( elem.parentNode ) { + + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + empty: function( elem ) { + + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + parent: function( elem ) { + return !Expr.pseudos.empty( elem ); + }, + + header: function( elem ) { + return rheader.test( elem.nodeName ); + }, + + input: function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + button: function( elem ) { + return nodeName( elem, "input" ) && elem.type === "button" || + nodeName( elem, "button" ); + }, + + text: function( elem ) { + var attr; + return nodeName( elem, "input" ) && elem.type === "text" && + + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + first: createPositionalPseudo( function() { + return [ 0 ]; + } ), + + last: createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + even: createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + odd: createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + lt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i; + + if ( argument < 0 ) { + i = argument + length; + } else if ( argument > length ) { + i = length; + } else { + i = argument; + } + + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + gt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos.nth = Expr.pseudos.eq; + +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + type: match[ 0 ].replace( rtrimCSS, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + find.error( selector ) : + + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + if ( skip && nodeName( elem, skip ) ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = outerCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + outerCache[ key ] = newCache; + + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + find( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, matcherOut, + preMap = [], + postMap = [], + preexisting = results.length, + + elems = seed || + multipleContexts( selector || "*", + context.nodeType ? [ context ] : context, [] ), + + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems; + + if ( matcher ) { + + matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + [] : + + results; + + matcher( matcherIn, matcherOut, context, xml ); + } else { + matcherOut = matcherIn; + } + + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + + var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + if ( matcher[ expando ] ) { + + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + tokens.slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrimCSS, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + elems = seed || byElement && Expr.find.TAG( "*", outermost ), + + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + outermostContext = context == document || context || outermost; + } + + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + push.call( results, elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + if ( bySet ) { + + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + if ( seed ) { + unmatched.push( elem ); + } + } + } + + matchedCount += i; + + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + setMatched = condense( setMatched ); + } + + push.apply( results, setMatched ); + + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + jQuery.uniqueSort( results ); + } + } + + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +function compile( selector, match ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + cached = compilerCache( selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + cached.selector = selector; + } + return cached; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + if ( match.length === 1 ) { + + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find.ID( + token.matches[ 0 ].replace( runescape, funescape ), + context + ) || [] )[ 0 ]; + if ( !context ) { + return results; + + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && + testContext( context.parentNode ) || context + ) ) ) { + + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +} + +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +setDocument(); + +support.sortDetached = assert( function( el ) { + + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +jQuery.find = find; + +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.unique = jQuery.uniqueSort; + +find.compile = compile; +find.select = select; +find.setDocument = setDocument; +find.tokenize = tokenize; + +find.escape = jQuery.escapeSelector; +find.getText = jQuery.text; +find.isXML = jQuery.isXMLDoc; +find.selectors = jQuery.expr; +find.support = jQuery.support; +find.uniqueSort = jQuery.uniqueSort; + +} )(); + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + +var rootjQuery, + + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + if ( !selector ) { + return this; + } + + root = root || rootjQuery; + + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + if ( match && ( match[ 1 ] || !context ) ) { + + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + } else { + return this.constructor( context ).find( selector ); + } + + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +init.prototype = jQuery.fn; + +rootjQuery = jQuery( document ); + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + index: function( elem ) { + + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + return indexOf.call( this, + + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +jQuery.Callbacks = function( options ) { + + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var + firing, + + memory, + + fired, + + locked, + + list = [], + + queue = [], + + firingIndex = -1, + + fire = function() { + + locked = locked || options.once; + + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + firingIndex = list.length; + memory = false; + } + } + } + + if ( !options.memory ) { + memory = false; + } + + firing = false; + + if ( locked ) { + + if ( memory ) { + list = []; + + } else { + list = ""; + } + } + }, + + self = { + + add: function() { + if ( list ) { + + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + fired: function() { + return !!fired; + } + }; + + return self; +}; + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + } else { + + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + } catch ( value ) { + + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + pipe: function( ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + then = returned && + + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + if ( isFunction( then ) ) { + + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + } else { + + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + } else { + + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + ( special || deferred.resolveWith )( that, args ); + } + }, + + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.error ); + } + + if ( depth + 1 >= maxDepth ) { + + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + if ( depth ) { + process(); + } else { + + if ( jQuery.Deferred.getErrorHook ) { + process.error = jQuery.Deferred.getErrorHook(); + + } else if ( jQuery.Deferred.getStackHook ) { + process.error = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + promise[ tuple[ 1 ] ] = list.add; + + if ( stateString ) { + list.add( + function() { + + state = stateString; + }, + + tuples[ 3 - i ][ 2 ].disable, + + tuples[ 3 - i ][ 3 ].disable, + + tuples[ 0 ][ 2 ].lock, + + tuples[ 0 ][ 3 ].lock + ); + } + + list.add( tuple[ 3 ].fire ); + + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + promise.promise( deferred ); + + if ( func ) { + func.call( deferred, deferred ); + } + + return deferred; + }, + + when: function( singleValue ) { + var + + remaining = arguments.length, + + i = remaining, + + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + primary = jQuery.Deferred(), + + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, asyncError ) { + + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, + error.stack, asyncError ); + } +}; + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + isReady: false, + + readyWait: 1, + + ready: function( wait ) { + + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + jQuery.isReady = true; + + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + window.setTimeout( jQuery.ready ); + +} else { + + document.addEventListener( "DOMContentLoaded", completed ); + + window.addEventListener( "load", completed ); +} + +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + if ( raw ) { + fn.call( elems, value ); + fn = null; + + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + var value = owner[ this.expando ]; + + if ( !value ) { + value = {}; + + if ( acceptData( owner ) ) { + + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + } else { + + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + this.set( owner, key, value ); + + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + if ( Array.isArray( key ) ) { + + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + if ( elem && value === undefined ) { + + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + return; + } + + this.each( function() { + + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + elem = el || elem; + + return elem.style.display === "none" || + elem.style.display === "" && + + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + initial = initial / 2; + + unit = unit || initialInUnit[ 3 ]; + + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + dataPriv.set( elem, "display", display ); + } + } + } + + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + div.innerHTML = ""; + support.option = !!div.lastChild; +} )(); + +var wrapMap = { + + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + +function getAll( context, tag ) { + + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + if ( toType( elem ) === "object" ) { + + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + jQuery.merge( nodes, tmp.childNodes ); + + tmp = fragment.firstChild; + + tmp.textContent = ""; + } + } + } + + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + tmp = getAll( fragment.appendChild( elem ), "script" ); + + if ( attached ) { + setGlobalEval( tmp ); + } + + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + if ( typeof types === "object" ) { + + if ( typeof selector !== "string" ) { + + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + fn = data; + data = undefined; + } else { + + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + if ( !acceptData( elem ) ) { + return; + } + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + if ( !type ) { + continue; + } + + special = jQuery.event.special[ type ] || {}; + + type = ( selector ? special.delegateType : special.bindType ) || type; + + special = jQuery.event.special[ type ] || {}; + + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + jQuery.event.global[ type ] = true; + } + + }, + + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + if ( delegateCount && + + cur.nodeType && + + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + noBubble: true + }, + click: { + + setup: function( data ) { + + var el = this || data; + + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click", true ); + } + + return false; + }, + trigger: function( data ) { + + var el = this || data; + + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + return true; + }, + + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +function leverageNative( el, type, isSetup ) { + + if ( !isSetup ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + if ( !saved ) { + + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + this[ type ](); + result = dataPriv.get( this, type ); + dataPriv.set( this, type, false ); + + if ( saved !== result ) { + + event.stopImmediatePropagation(); + event.preventDefault(); + + return result; + } + + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + } else if ( saved ) { + + dataPriv.set( this, type, jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) ); + + event.stopPropagation(); + event.isImmediatePropagationStopped = returnTrue; + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + src.returnValue === false ? + returnTrue : + returnFalse; + + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + } else { + this.type = src; + } + + if ( props ) { + jQuery.extend( this, props ); + } + + this.timeStamp = src && src.timeStamp || Date.now(); + + this[ jQuery.expando ] = true; +}; + +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + + function focusMappedHandler( nativeEvent ) { + if ( document.documentMode ) { + + var handle = dataPriv.get( this, "handle" ), + event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; + + handle( nativeEvent ); + + if ( event.target === event.currentTarget ) { + + handle( event ); + } + } else { + + jQuery.event.simulate( delegateType, nativeEvent.target, + jQuery.event.fix( nativeEvent ) ); + } + } + + jQuery.event.special[ type ] = { + + setup: function() { + + var attaches; + + leverageNative( this, type, true ); + + if ( document.documentMode ) { + + attaches = dataPriv.get( this, delegateType ); + if ( !attaches ) { + this.addEventListener( delegateType, focusMappedHandler ); + } + dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); + } else { + + return false; + } + }, + trigger: function() { + + leverageNative( this, type ); + + return true; + }, + + teardown: function() { + var attaches; + + if ( document.documentMode ) { + attaches = dataPriv.get( this, delegateType ) - 1; + if ( !attaches ) { + this.removeEventListener( delegateType, focusMappedHandler ); + dataPriv.remove( this, delegateType ); + } else { + dataPriv.set( this, delegateType, attaches ); + } + } else { + + return false; + } + }, + + _default: function( event ) { + return dataPriv.get( event.target, type ); + }, + + delegateType: delegateType + }; + + jQuery.event.special[ delegateType ] = { + setup: function() { + + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ); + + if ( !attaches ) { + if ( document.documentMode ) { + this.addEventListener( delegateType, focusMappedHandler ); + } else { + doc.addEventListener( type, focusMappedHandler, true ); + } + } + dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ) - 1; + + if ( !attaches ) { + if ( document.documentMode ) { + this.removeEventListener( delegateType, focusMappedHandler ); + } else { + doc.removeEventListener( type, focusMappedHandler, true ); + } + dataPriv.remove( dataHolder, delegateType ); + } else { + dataPriv.set( dataHolder, delegateType, attaches ); + } + } + }; +} ); + +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + +var + + rnoInnerhtml = /\s*$/g; + +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + if ( hasScripts ) { + + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + jQuery.map( scripts, restoreScript ); + + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + jQuery.cleanData( getAll( elem, false ) ); + + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var rcustomProp = /^--/; + +var getStyles = function( elem ) { + + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + +( function() { + + function computeStyleTests() { + + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + if ( !div.style ) { + return; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "box-sizing:content-box;border:1px solid"; + + tr.style.height = "1px"; + trChild.style.height = "9px"; + + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + isCustomProp = rcustomProp.test( name ), + + style = elem.style; + + computed = computed || getStyles( elem ); + + if ( computed ) { + + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( isCustomProp && ret ) { + + ret = ret.replace( rtrimCSS, "$1" ) || undefined; + } + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + ret + "" : + ret; +} + +function addGetHookIf( conditionFn, hookFn ) { + + return { + get: function() { + if ( conditionFn() ) { + + delete this.get; + return; + } + + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +function vendorPropName( name ) { + + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + +var + + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + var matches = rcssNum.exec( value ); + return matches ? + + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0, + marginDelta = 0; + + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + if ( box === "margin" ) { + marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + if ( !isBorderBox ) { + + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + } else { + + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + if ( !isBorderBox && computedVal >= 0 ) { + + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + ) ) || 0; + } + + return delta + marginDelta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + var styles = getStyles( elem ), + + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + if ( ( !support.boxSizingReliable() && isBorderBox || + + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + val === "auto" || + + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + val = parseFloat( val ) || 0; + + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + val + ) + ) + "px"; +} + +jQuery.extend( { + + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + cssNumber: { + animationIterationCount: true, + aspectRatio: true, + borderImageSlice: true, + columnCount: true, + flexGrow: true, + flexShrink: true, + fontWeight: true, + gridArea: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnStart: true, + gridRow: true, + gridRowEnd: true, + gridRowStart: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + scale: true, + widows: true, + zIndex: true, + zoom: true, + + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeMiterlimit: true, + strokeOpacity: true + }, + + cssProps: {}, + + style: function( elem, name, value, extra ) { + + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + if ( value !== undefined ) { + type = typeof value; + + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + type = "number"; + } + + if ( value == null || value !== value ) { + return; + } + + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + result = jQuery.css( tween.elem, tween.prop, "" ); + + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +jQuery.fx.step = {}; + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + if ( isBox && elem.nodeType === 1 ) { + + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + propTween = false; + for ( prop in orig ) { + + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + if ( toggle ) { + dataShow.hidden = !hidden; + } + + if ( hidden ) { + showHide( [ elem ], true ); + } + + anim.done( function() { + + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + if ( percent < 1 && length ) { + return remaining; + } + + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + data.finish = true; + + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + _default: 400 +}; + +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + support.checkOn = input.value !== ""; + + support.optSelected = opt.selected; + + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + if ( cur.indexOf( " " + className + " " ) < 0 ) { + cur += className + " "; + } + } + + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + removeClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + while ( cur.indexOf( " " + className + " " ) > -1 ) { + cur = cur.replace( " " + className + " ", " " ); + } + } + + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var classNames, className, i, self, + type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + classNames = classesToArray( value ); + + return this.each( function() { + if ( isValidValue ) { + + self = jQuery( this ); + + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + dataPriv.set( this, "__className__", className ); + } + + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + for ( ; i < max; i++ ) { + option = options[ i ]; + + if ( ( option.selected || i === index ) && + + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + value = jQuery( option ).val(); + + if ( one ) { + return value; + } + + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + } + + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + add( prefix, v ); + + } else { + + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + add( prefix, obj ); + } +} + +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\ + + prefilters = {}, + + transports = {}, + + allTypes = "*/".concat( "*" ), + + originAnchor = document.createElement( "a" ); + +originAnchor.href = location.href; + +function addToPrefiltersOrTransports( structure ) { + + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + while ( ( dataType = dataTypes[ i++ ] ) ) { + + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + finalDataType = finalDataType || firstDataType; + } + + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + dataTypes = s.dataTypes.slice(); + + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + if ( current === "*" ) { + + current = prev; + + } else if ( prev !== "*" && prev !== current ) { + + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + if ( !conv ) { + for ( conv2 in converters ) { + + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + if ( conv === true ) { + conv = converters[ conv2 ]; + + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + if ( conv !== true ) { + + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + active: 0, + + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + converters: { + + "* text": String, + + "text html": true, + + "text json": JSON.parse, + + "text xml": jQuery.parseXML + }, + + flatOptions: { + url: true, + context: true + } + }, + + ajaxSetup: function( target, settings ) { + return settings ? + + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + ajax: function( url, options ) { + + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + options = options || {}; + + var transport, + + cacheURL, + + responseHeadersString, + responseHeaders, + + timeoutTimer, + + urlAnchor, + + completed, + + fireGlobals, + + i, + + uncached, + + s = jQuery.ajaxSetup( {}, options ), + + callbackContext = s.context || s, + + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + statusCode = s.statusCode || {}, + + requestHeaders = {}, + requestHeadersNames = {}, + + strAbort = "canceled", + + jqXHR = { + readyState: 0, + + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + jqXHR.always( map[ jqXHR.status ] ); + } else { + + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + deferred.promise( jqXHR ); + + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + s.type = options.method || options.type || s.method || s.type; + + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + try { + urlAnchor.href = s.url; + + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + s.crossDomain = true; + } + } + + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + if ( completed ) { + return jqXHR; + } + + fireGlobals = jQuery.event && s.global; + + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + s.type = s.type.toUpperCase(); + + s.hasContent = !rnoContent.test( s.type ); + + cacheURL = s.url.replace( rhash, "" ); + + if ( !s.hasContent ) { + + uncached = s.url.slice( cacheURL.length ); + + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + delete s.data; + } + + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + s.url = cacheURL + uncached; + + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + return jqXHR.abort(); + } + + strAbort = "abort"; + + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + if ( completed ) { + return jqXHR; + } + + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + if ( completed ) { + throw e; + } + + done( -1, e ); + } + } + + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + if ( completed ) { + return; + } + + completed = true; + + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + transport = undefined; + + responseHeadersString = headers || ""; + + jqXHR.readyState = status > 0 ? 4 : 0; + + isSuccess = status >= 200 && status < 300 || status === 304; + + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { + s.converters[ "text script" ] = function() {}; + } + + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + if ( isSuccess ) { + + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + } else if ( status === 304 ) { + statusText = "notmodified"; + + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + 0: 200, + + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + +callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; +}; + +xhr.onload = callback(); +errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + +if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; +} else { + xhr.onreadystatechange = function() { + + if ( xhr.readyState === 4 ) { + + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; +} + +callback = callback( "abort" ); + +try { + + xhr.send( options.hasContent && options.data || null ); +} catch ( e ) { + + if ( callback ) { + throw e; + } +} +}, + +abort: function() { +if ( callback ) { + callback(); +} +} +}; +} +} ); + +jQuery.ajaxPrefilter( function( s ) { +if ( s.crossDomain ) { +s.contents.script = false; +} +} ); + +jQuery.ajaxSetup( { +accepts: { +script: "text/javascript, application/javascript, " + +"application/ecmascript, application/x-ecmascript" +}, +contents: { +script: /\b(?:java|ecma)script\b/ +}, +converters: { +"text script": function( text ) { +jQuery.globalEval( text ); +return text; +} +} +} ); + +jQuery.ajaxPrefilter( "script", function( s ) { +if ( s.cache === undefined ) { +s.cache = false; +} +if ( s.crossDomain ) { +s.type = "GET"; +} +} ); + +jQuery.ajaxTransport( "script", function( s ) { + +if ( s.crossDomain || s.scriptAttrs ) { +var script, callback; +return { +send: function( _, complete ) { +script = jQuery( " + + + diff --git a/data/www/edit.html b/data/www/edit.html new file mode 100644 index 0000000..9f29e48 --- /dev/null +++ b/data/www/edit.html @@ -0,0 +1,191 @@ + + + + Edit file + + + + + + + +

Edit file

+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/data/www/edit_old.html b/data/www/edit_old.html new file mode 100644 index 0000000..e76f413 --- /dev/null +++ b/data/www/edit_old.html @@ -0,0 +1,151 @@ + + + + Edit file + + + + + + +

Edit file

+
+ Editing file: {{SAVE_PATH_INPUT}} +
+
+
+ +
+
+ {{SAVE_PATH_INPUT}} + + + + +
+
+
+ + + + + + \ No newline at end of file diff --git a/data/www/failed.html b/data/www/failed.html new file mode 100644 index 0000000..cfcf05e --- /dev/null +++ b/data/www/failed.html @@ -0,0 +1,33 @@ + + + + Update Failed + + + + + + + +
+

The update has failed.

+
+ +
+ + + + \ No newline at end of file diff --git a/data/www/files.html b/data/www/files.html new file mode 100644 index 0000000..54f37df --- /dev/null +++ b/data/www/files.html @@ -0,0 +1,276 @@ + + + + File Manager + + + + + + + + + +
+

File Manager

+ +
+ File list +
+

  Total: {{FS_TOTAL_BYTES}}, Used: {{FS_USED_BYTES}}, Available: {{FS_FREE_BYTES}}

+
+ + {{LISTED_FILES}} +
Listing: Size:
+
+
+ +
+ +
+ File upload +
+
+ + + + + + + + + + + +
+
+    + +
+
+
+ +
+
+
+ +
+ +
+ Edit file +
+
+ + + +
+
+
+
+ +
+ +
+ Delete file +
+
+ + + +
+
+
+
+ +
+ + + + +
+ + + \ No newline at end of file diff --git a/data/www/home.html b/data/www/home.html new file mode 100644 index 0000000..84f6450 --- /dev/null +++ b/data/www/home.html @@ -0,0 +1,303 @@ + + + + System Summary + + + + + + + +

System Summary

+
+ +
+ Booth Overview +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ System Information +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+
+ + +
+ Network / WiFi +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ Bluetoth LE +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ Luma Stiks +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + + + \ No newline at end of file diff --git a/data/www/index.html b/data/www/index.html new file mode 100644 index 0000000..6c6f81a --- /dev/null +++ b/data/www/index.html @@ -0,0 +1,29 @@ + + + + + ESP32 AP + + + +

Welcome to ESP32 AP

+

This is a simple web server running on ESP32 in Access Point mode.

+
+ + + \ No newline at end of file diff --git a/data/www/lights.html b/data/www/lights.html new file mode 100644 index 0000000..01a004b --- /dev/null +++ b/data/www/lights.html @@ -0,0 +1,527 @@ + + + + + + + + + + + + + +
+

Lights Configuration

+
+ Countdown ( Constant Light ) +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ +      + +
+
+
+
+ +
+
+ Saved Animation Profiles +
+
+ + +
+
+ + + +
+
+
+
+ +
+ + + + diff --git a/data/www/navbar.html b/data/www/navbar.html new file mode 100644 index 0000000..406a174 --- /dev/null +++ b/data/www/navbar.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/data/www/ok.html b/data/www/ok.html new file mode 100644 index 0000000..6df4dda --- /dev/null +++ b/data/www/ok.html @@ -0,0 +1,34 @@ + + + + Update Success + + + + + + + +
+

The update was successful.

+
+ +
+ + + + \ No newline at end of file diff --git a/data/www/setup.html b/data/www/setup.html new file mode 100644 index 0000000..18b6fb9 --- /dev/null +++ b/data/www/setup.html @@ -0,0 +1,385 @@ + + + + Booth Configuration Tools + + + + + + + + +

System Setup

+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ Test Tool (Single LED) + +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ Luma Stiks Found + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/data/www/upgrade.html b/data/www/upgrade.html new file mode 100644 index 0000000..f7da5e9 --- /dev/null +++ b/data/www/upgrade.html @@ -0,0 +1,234 @@ + + + + + + Firmware Upgrade + + + + +
+

Firmware Upgrade

+ +
+ + Disconnected +
+ +
+

Current Version: -

+

Latest Version: -

+
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/data/www/wifi.html b/data/www/wifi.html new file mode 100644 index 0000000..06e3df9 --- /dev/null +++ b/data/www/wifi.html @@ -0,0 +1,159 @@ + + + + + + Wifi Access + + + + + +

Wifi Access

+
+
+ + +
+ + + + + +
+ + + + diff --git a/diagram.json b/diagram.json new file mode 100644 index 0000000..3cf5955 --- /dev/null +++ b/diagram.json @@ -0,0 +1,8 @@ +{ + "version": 1, + "author": "David P", + "editor": "wokwi", + "parts": ["board-esp32-s3-devkitc-1"], + "connections": [ [ "esp:TX", "$serialMonitor:RX", "", [] ], [ "esp:RX", "$serialMonitor:TX", "", [] ] ], + "dependencies": {} +} \ No newline at end of file diff --git a/docs/!instructions.txt b/docs/!instructions.txt new file mode 100644 index 0000000..95720bd --- /dev/null +++ b/docs/!instructions.txt @@ -0,0 +1,18 @@ + +Choose Control App: + 1) Open app-events.json + 2) Change index to corresponding app + a. 0=DSLRBooth, 1=...., 2=..... + +Front Constant Light: ( to enable the light) + 1) Open led-devices.json + 2) Change "front-light", "en" to true + +Rear Constant Light: + 1) Open led-devices.json + 2) Change "rear-light", "en" to true + + + + +**anim-profiles \ No newline at end of file diff --git a/docs/DSLRBooth Mode Wifi no bluetooth.md b/docs/DSLRBooth Mode Wifi no bluetooth.md new file mode 100644 index 0000000..b99510b --- /dev/null +++ b/docs/DSLRBooth Mode Wifi no bluetooth.md @@ -0,0 +1,2 @@ +# DSLRBooth Mode Wifi no bluetooth + diff --git a/docs/Manual Mode.md b/docs/Manual Mode.md new file mode 100644 index 0000000..c1f0c63 --- /dev/null +++ b/docs/Manual Mode.md @@ -0,0 +1,3 @@ +# Manual Mode + +asdad diff --git a/docs/Sys Report.txt b/docs/Sys Report.txt new file mode 100644 index 0000000..74c6f70 --- /dev/null +++ b/docs/Sys Report.txt @@ -0,0 +1,51 @@ +********** APP CONTROL ********** +App: DSLRBooth +Mode: + +********** PERIPHERALS ********** +RGB Strip1 Active: true, Size: 164, Core: 0, I2S_Ch: 0 + Shift: 0, Offset: 0, RGB Order: grb, Pin: 30 +RGB Strip2 Active: true, Size: 164, Core: 0, I2S_Ch: 0 + Shift: 0, Offset: 0, RGB Order: grb, Pin: 30 +Front Light Active: true, Relay: 0, Freq: 500 +Rear Light Active: false, Relay: 1, Freq: 500 +Buzzer: On +OLED: Off + +********** NETWORK ********** +Wifi Client Active: true, Status: Connected + SSID: ATA, RSSi:-107, Ch:12, Encryp: asdasda, + IP: 192.168.0.15, MAC: XX:XX:XX:XX:XX, +Wifi AP Active: false, Status: Client connected, + IP: 192.168.4.1 + +********** Chip ********** +Clock: 240Mhz, RAM: asdfas, Used: 23423, Free: 23423 +ID: 23:23:43:32:23:43 + +********** Bluetooth LE ********** +Active: true, Server SSID: ATA_COMMXX, Connected: +SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +********** FAN / T SENSOR ********** +Fan Active: true, Relay: 3 +TSensor Active: true, Setpoint1: 80, Setpoint2: 85 +T Now: 78.2F + +********** 12V Monitor ********** +Volt: 12.2V +********** Buttons ********** + + +********** Touch Sensors ********** + +********** 433 RX/TX ********** +RX Active: true +TX Active: true + +********** Tasks & CPU Usage ********** +Task1... CPU Usage: 23 Priority:3 +Task2... +Task3... diff --git a/docs/Todo (add).doc b/docs/Todo (add).doc new file mode 100644 index 0000000..86d8e0d --- /dev/null +++ b/docs/Todo (add).doc @@ -0,0 +1,22 @@ + + +2) Make I2S buffer setting automatic based on pixel size ---------------- DONE + +3) Add Address Input box for firmware web location + +4) cpu / thread water mark diagnostics + +5) ble multi client + +6) Strip 2 simulataneous function Test + +7) Add button to control Rear Lights + + +**** Add LED Color Indication on BLE Connection established and disconnect + +*** + + + +**** Saving from web to LittleFS.... Write to temporary file then rename. \ No newline at end of file diff --git a/docs/Todo.(fixes)doc b/docs/Todo.(fixes)doc new file mode 100644 index 0000000..0f94156 --- /dev/null +++ b/docs/Todo.(fixes)doc @@ -0,0 +1,10 @@ +1) Helio and Lumia Lighting Logic + +2) Rear Light Button Ramp control + +3) Better Tunes + +4) Save Wifi Creds in Flash...not LittleFS + +5) Config page, + Booth Type, Front active, Rear Active, \ No newline at end of file diff --git a/docs/cli commands.txt b/docs/cli commands.txt new file mode 100644 index 0000000..2714777 --- /dev/null +++ b/docs/cli commands.txt @@ -0,0 +1,23 @@ +1. set-relay -value -freq +2. set-event -index +3. set-front-light -value -freq +4. playtune -index x + playtune "dasdasafsdfasdf" +5. readtemp -c +6. restart +7. set-bright +8. readfile -f filename +9. list directory +10. trigger button +11. echo +12. ble-msg +13. ping +14. post ble command +15. post tcp command +16. set wifi creds +17. start AP +18. start ble +19. test event anim colmain colbase density speed +20. send rf +21. get 12V value +22. \ No newline at end of file diff --git a/esp32s3_atabooth_8mb.code-workspace b/esp32s3_atabooth_8mb.code-workspace new file mode 100644 index 0000000..2ce8fdd --- /dev/null +++ b/esp32s3_atabooth_8mb.code-workspace @@ -0,0 +1,74 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../esp32s3_module_8mb" + } + ], + "settings": { + "files.associations": { + "*.service": "ini", + "new": "cpp", + "array": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "condition_variable": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "set": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp" + }, + "liveServer.settings.multiRootWorkspaceName": "esp32s3_atabooth_8mb" + } +} \ No newline at end of file diff --git a/firmware_update/GenUpdate.py b/firmware_update/GenUpdate.py new file mode 100644 index 0000000..1356185 --- /dev/null +++ b/firmware_update/GenUpdate.py @@ -0,0 +1,165 @@ +import os +import shutil +import hashlib +import json + +def copy_folder_to_destination(src_path, dest_path, skip_dirs=None, skip_files=None): + # Ensure the destination directory exists + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + + # Remove existing folder if present + if os.path.exists(dest_path): + shutil.rmtree(dest_path) + + # Create destination directory + os.makedirs(dest_path) + + # Walk through source directory + for root, dirs, files in os.walk(src_path): + # Remove directories to skip from dirs list + if skip_dirs: + dirs[:] = [d for d in dirs if d not in skip_dirs] + + # Calculate relative path + rel_path = os.path.relpath(root, src_path) + dest_dir = os.path.join(dest_path, rel_path) + + # Create corresponding destination directory + os.makedirs(dest_dir, exist_ok=True) + + # Copy files that aren't in skip_files + for file in files: + if skip_files and file in skip_files: + continue + src_file = os.path.join(root, file) + dest_file = os.path.join(dest_dir, file) + shutil.copy2(src_file, dest_file) + +def calculate_md5(file_path): + hash_md5 = hashlib.md5() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + +def get_file_size(file_path): + return os.path.getsize(file_path) + +def update_json_file(json_array, folder_path): + # Create new data array for files + file_array = [] + + # Walk through the copied folder and collect file details + for root, _, files in os.walk(folder_path): + for file in files: + file_path = os.path.join(root, file) + relative_path = os.path.relpath(file_path, folder_path) + + # Replace backslashes with forward slashes + relative_path = relative_path.replace('\\', '/') + + file_entry = { + "remote": os.path.join("data/", relative_path), + "local": os.path.join("/", relative_path), + "md5": calculate_md5(file_path), + "size": get_file_size(file_path) + } + file_array.append(file_entry) + + # Replace the contents of the input json_array with new data + json_array.clear() + json_array.extend(file_array) + + +def update_files(src_path, dest_path, skip_dirs=None, skip_files=None): + # Check if the source folder exists + if not os.path.isdir(src_path) or not os.path.isdir(dest_path): + print("Invalid folder path!") + return + + # Copy all data contents + copy_folder_to_destination(src_path, dest_path, skip_dirs, skip_files) + print("Folder copied successfully.") + + + +def main(): + + here_path = os.path.dirname(os.path.abspath(__file__)) + project_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # Path of the folder to copy (you can modify this) + src_folder_name = "data" + src_path = os.path.join(project_path, src_folder_name) + #print(f"source path: {src_path}") + + # Path of the destination folder + dest_folder_name = "firmware_update\\latest\\data" + dest_path = os.path.join(project_path, dest_folder_name) + #print(f"destination path: {dest_path}") + + # Path of the firmware binary file + bin_name = ".pio\\build\\esp32s3dev\\firmware.bin" + + # Skip these directories + skip_dirs = ["boards", "booths"] + + # Skip these files + skip_files = ["wifi.json", "system.json", "luma-stiks.json", ] + + + update_files = input("Do you want to update the files? (y/n): ") + + # *********************** Copy Data Files *********************** + if update_files.lower() == "y": + update_files(src_path, dest_path, skip_dirs, skip_files) + + + # *********************** Copy Binary file *********************** + update_firmware = input("Do you want to update the firmware? (y/n): ") + if update_firmware.lower() == "y": + # Copy firmware.bin to the destination + bin_path = os.path.join(project_path, bin_name) + shutil.copy(bin_path, here_path) + #print(f"firmware path: {bin_path}") + print("firmware.bin copied successfully.") + + + + # *********************** Process update.json *********************** + # Update the JSON file + json_path = os.path.join(here_path, "update.json") + print(f"json path: {json_path}") + + # Read existing JSON + with open(json_path, "r") as f: + try: + json_doc = json.load(f) + except json.JSONDecodeError: + print("Invalid JSON file!") + return + + # process the files array + #if update_files.lower() == "y": + json_files_array = json_doc["files"] + update_json_file(json_files_array, dest_path) + #print(f"Folder {os.path.basename(src_path)} processed successfully.") + + + # *********************** Process firmwware.bin in update.json *********************** + # process the firmware + if update_firmware.lower() == "y": + json_firmware = json_doc["firmware"] + firmware_path = os.path.join(here_path, "firmware.bin") + json_firmware["md5"] = calculate_md5(firmware_path) + json_firmware["size"] = get_file_size(firmware_path) + + + # Write updated JSON + with open(json_path, "w") as f: + json.dump(json_doc, f, indent=4) + + print("Update JSON files created successfully.") + +if __name__ == "__main__": + main() diff --git a/firmware_update/UploadToGoogle.py b/firmware_update/UploadToGoogle.py new file mode 100644 index 0000000..d5747a1 --- /dev/null +++ b/firmware_update/UploadToGoogle.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +import os +import datetime +from pathlib import Path +from google.cloud import storage + +# ============================================================================= +# CONFIGURATION CONSTANTS +# ============================================================================= + +CREATE_BACKUP = False +UPLOAD_FIRMWARE = True +UPLOAD_MANIFEST = True +UPLOAD_DATA = True + + +# The name of your Google Cloud Storage bucket. +GCS_BUCKET_NAME = 'boothifier' # <-- Change this + +# The destination directory (prefix) inside your bucket. +# For example, 'release' or 'prod'. Use an empty string ('') to use the bucket root. +DESTINATION_DIR = 'latest' # <-- Change this (or leave '' for bucket root) +BACKUPS_DIR = 'backups' + +LOCAL_ROOT_PATH = Path(__file__).parent.resolve() + +# Path to your Google Cloud credentials JSON file. +GOOGLE_APPLICATION_CREDENTIALS_PATH = str(LOCAL_ROOT_PATH / 'loyal-column-439819-e3-8cddff2ee2c2.json') + + +# Local path to the firmware file. +LOCAL_FIRMWARE_PATH = str(LOCAL_ROOT_PATH / 'firmware.bin') # <-- Change this if needed +LOCAL_MANIFEST_PATH = str(LOCAL_ROOT_PATH / 'update.json') # <-- Change this if needed + +# Local path to the data directory. +LOCAL_DATA_DIRECTORY = 'data' # <-- Change this if needed + +# ============================================================================= +# SET UP GOOGLE CLOUD CREDENTIALS +# ============================================================================= +os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = GOOGLE_APPLICATION_CREDENTIALS_PATH + +# ============================================================================= +# HELPER FUNCTIONS +# ============================================================================= + +def backup_existing_files(bucket, destination_prefix, backups_prefix, backup_folder): + """ + Copy every blob in the bucket with a name starting with destination_prefix + (except any files already in a backup folder) into a backup folder under backups_prefix. + + Files will be copied into: + // + """ + blobs = bucket.list_blobs(prefix=destination_prefix) + for blob in blobs: + # Compute the file's relative path by removing the destination_prefix. + if destination_prefix: + # Remove the destination_prefix and any leading slashes. + relative_path = blob.name[len(destination_prefix):].lstrip('/') + else: + relative_path = blob.name + + # Construct the new blob name in the backup folder under backups_prefix. + if backups_prefix: + new_blob_name = f"{backups_prefix}/{backup_folder}/{relative_path}" + else: + new_blob_name = f"{backup_folder}/{relative_path}" + + print(f"Backing up: copying '{blob.name}' to '{new_blob_name}'") + bucket.copy_blob(blob, bucket, new_blob_name) + +def upload_file(bucket, local_path, destination_blob_name): + """ + Upload a single file to the bucket with the specified blob name. + Overwrites the blob if it already exists. + """ + print(f"Uploading file '{local_path}' to '{destination_blob_name}'") + blob = bucket.blob(destination_blob_name) + # Set Cache-Control header to force clients to always fetch fresh content. + blob.cache_control = 'private, max-age=0, no-transform' + blob.upload_from_filename(local_path) + +def upload_directory(bucket, local_directory, destination_prefix): + """ + Recursively upload the contents of the local directory to the bucket under + destination_prefix. The directory structure is preserved. + """ + for root, _, files in os.walk(local_directory): + for file in files: + file_local_path = os.path.join(root, file) + # Compute the relative path of the file from the base directory. + rel_path = os.path.relpath(file_local_path, local_directory) + if destination_prefix: + dest_blob_name = f"{destination_prefix}/{rel_path}" + else: + dest_blob_name = rel_path + upload_file(bucket, file_local_path, dest_blob_name) + +# ============================================================================= +# MAIN FUNCTION +# ============================================================================= + +def main(): + # Initialize the Google Cloud Storage client. + client = storage.Client() + bucket = client.bucket(GCS_BUCKET_NAME) + + # Normalize the destination prefix by stripping any trailing slashes. + destination_prefix = DESTINATION_DIR.strip('/') + backups_prefix = BACKUPS_DIR.strip('/') + + if(CREATE_BACKUP): + # Create a backup folder name with a timestamp. + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + backup_folder = f"backup_{timestamp}" + print(f"Creating backup folder '{backup_folder}' inside '{destination_prefix}' and copying existing files...") + backup_existing_files(bucket, destination_prefix, backups_prefix, backup_folder) + + + if(UPLOAD_FIRMWARE): + print("Uploading firmware...") + # Upload the firmware file. + if destination_prefix: + firmware_destination = f"{destination_prefix}/firmware.bin" + else: + firmware_destination = "firmware.bin" + upload_file(bucket, LOCAL_FIRMWARE_PATH, firmware_destination) + + if(UPLOAD_MANIFEST): + print("Uploading manifest...") + # Upload the manifest. + if destination_prefix: + manifest_destination = f"{destination_prefix}/update.json" + else: + manifest_destination = "update.json" + upload_file(bucket, LOCAL_MANIFEST_PATH, manifest_destination) + + if(UPLOAD_DATA): + print("Uploading data directory...") + # Upload the data directory. + if destination_prefix: + data_destination = f"{destination_prefix}/data" + else: + data_destination = "data" + upload_directory(bucket, LOCAL_DATA_DIRECTORY, data_destination) + + + print("Upload complete.") + +if __name__ == '__main__': + main() diff --git a/firmware_update/latest/data/ata-boothifier-upgrade.html b/firmware_update/latest/data/ata-boothifier-upgrade.html new file mode 100644 index 0000000..3821ad8 --- /dev/null +++ b/firmware_update/latest/data/ata-boothifier-upgrade.html @@ -0,0 +1,501 @@ + + + + + + ATA Firmware Update + + + + +

ATA Firmware Update

+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ + +
+ + +
+ + + + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ +
+ + + + diff --git a/firmware_update/latest/data/css/global-style.css b/firmware_update/latest/data/css/global-style.css new file mode 100644 index 0000000..a4edce2 --- /dev/null +++ b/firmware_update/latest/data/css/global-style.css @@ -0,0 +1,79 @@ +body { + /*background-color: #f7f7f7;*/ + font-family: Tahoma, Arial, sans-serif; + font-size: small; + margin: 0; + display: flex; + min-height: 100vh; +} +.main-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 20px; +} +h1 { + margin-top: 0; +} +input{ + cursor:pointer; +} +input[type="number"]{ + width: 100px; +} +#submit { + width:120px; +} +select{ + width:160px; +} +#select-path { + width:250px; +} +#select-dir { + width:90px; +} +#spacer-50 { + height: 50px; +} +#spacer-20 { + height: 20px; +} +#spacer-10 { + height: 10px; +} +table { + /*background-color: #dddddd;*/ + border-collapse: collapse; + width:600px; + margin: 0 auto; + overflow: visible; +} +td, th { + /*border: 1px solid #dddddd;*/ + text-align: left; + padding: 2px; +} +#first_td_th { + width:400px; +} +fieldset { + width:620px; + /*background-color: #f7f7f7;*/ + border-radius: 10px; + border-color: blue; +} +#format-notice { + color: #ff0000; +} +legend { + display: flex; + justify-content: center; + background-color:white; + background-blend-mode: darken; + border-radius: 10px; + padding: 1px 8px 2px 8px; + border-style: solid; + border-width: 1.0; +} \ No newline at end of file diff --git a/firmware_update/latest/data/css/nav.css b/firmware_update/latest/data/css/nav.css new file mode 100644 index 0000000..f74f175 --- /dev/null +++ b/firmware_update/latest/data/css/nav.css @@ -0,0 +1,72 @@ +.navbar { + width: 100%; + background-color: black; + border-bottom: 2px solid white; + display: flex; + justify-content: flex-start; + align-items: center; + position: sticky; + top: 0; + z-index: 1000; + } + .navbar-left { + padding: 0 10px; + } + .navbar ul { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + position: relative; /* Ensure relative positioning for the submenu */ + } + .navbar ul li { + float: left; + position: relative; /* Ensure relative positioning for the submenu */ + } + .navbar ul li a { + display: block; + color: white; + text-align: center; + padding: 14px 16px; + text-decoration: none; + border-right: 1px solid white; + } + .navbar ul li a:hover { + background-color: grey; + } + .navbar ul li a.disabled { + color: grey; + pointer-events: none; + } + .navbar ul .submenu { + display: none; + position: absolute; + top: 100%; + left: 0; + background-color: black; + list-style-type: none; + margin: 0; + padding: 0; + border-top: 2px solid white; + z-index: 1000; + } + .navbar ul .submenu li { + float: none; + border-right: none; + } + .navbar ul .submenu li a { + padding: 10px 16px; + border-bottom: 1px solid white; + } + .navbar ul .submenu li a:hover { + background-color: grey; + } + .navbar ul li:hover > .submenu { + display: block; + } + .nav-image { + height: 40px; + width: auto; + } + \ No newline at end of file diff --git a/firmware_update/latest/data/favicon.ico b/firmware_update/latest/data/favicon.ico new file mode 100644 index 0000000..3030261 Binary files /dev/null and b/firmware_update/latest/data/favicon.ico differ diff --git a/firmware_update/latest/data/images/atalogo.png b/firmware_update/latest/data/images/atalogo.png new file mode 100644 index 0000000..40f3ee6 Binary files /dev/null and b/firmware_update/latest/data/images/atalogo.png differ diff --git a/firmware_update/latest/data/images/favicon-32x32.png b/firmware_update/latest/data/images/favicon-32x32.png new file mode 100644 index 0000000..5118fd1 Binary files /dev/null and b/firmware_update/latest/data/images/favicon-32x32.png differ diff --git a/firmware_update/latest/data/js/event-box.js b/firmware_update/latest/data/js/event-box.js new file mode 100644 index 0000000..cb01b1b --- /dev/null +++ b/firmware_update/latest/data/js/event-box.js @@ -0,0 +1,442 @@ +class EventBox extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + +
+
+ Event0
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+ +
+
+
+ +
+
+ `; + this.Title = this.shadowRoot.querySelector('#center-text'); + this.hueSelect = this.shadowRoot.querySelector('#hue-selector'); + this.AnimationList = this.shadowRoot.getElementById('animation-list'); + this.Speed = this.shadowRoot.getElementById('speed'); + this.HueRange = this.shadowRoot.querySelector('#huerange'); + this.Param1 = this.shadowRoot.querySelector('#param1'); + this.Param2 = this.shadowRoot.querySelector('#param2'); + this.Check1 = this.shadowRoot.querySelector('#check1'); + this.Check2 = this.shadowRoot.querySelector('#check2'); + this.Check3 = this.shadowRoot.querySelector('#check3'); + this.Check4 = this.shadowRoot.querySelector('#check4'); + + this.AnimationListLabel = this.shadowRoot.querySelector('#list-label'); + this.SpeedLabel = this.shadowRoot.querySelector('#speed-label'); + this.HueRangeLabel = this.shadowRoot.querySelector('#huerange-label'); + this.Param1Label = this.shadowRoot.getElementById('param1-label'); + this.Param2Label = this.shadowRoot.getElementById('param2-label'); + this.Check1Label = this.shadowRoot.getElementById('check1-label'); + this.Check2Label = this.shadowRoot.getElementById('check2-label'); + this.Check3Label = this.shadowRoot.getElementById('check3-label'); + this.Check4Label = this.shadowRoot.getElementById('check4-label'); + + this.AnimationPropsJson; + + this.SpeedCaption = ""; + this.HueRangeCaption = ""; + this.Param1Caption = ""; + this.Param2Caption = ""; + this.Check1Caption = ""; + this.Check2Caption = ""; + this.Check3Caption = ""; + + this.setSpeedCaption(`Speed: `); + this.Speed.min = 0; + this.Speed.max = 100; + + this.setHueRangeCaption(`Hue Range: `); + this.HueRange.min = 0; + this.HueRange.max = 360; + + this.setParam1Caption(`Param1: `); + this.Param1.min = 0; + this.Param1.max = 100; + + this.setParam2Caption(`Param2: `); + this.Param2.min = 0; + this.Param2.max = 100; + + this.setCheck1Caption(`Check1`); + this.setCheck2Caption(`Check2`); + this.setCheck3Caption(`Check3`); + this.setCheck4Caption(`50% Lum`); + + this.Index = 0; + + this.Speed.addEventListener('input', () => { this.updateSpeedLabel(); }); + this.HueRange.addEventListener('input', () => { this.updateHueRangeLabel(); }); + this.Param1.addEventListener('input', () => { this.updateParam1Label(); }); + this.Param2.addEventListener('input', () => { this.updateParam2Label(); }); + + this.TryButton = this.shadowRoot.querySelector('#try-button'); + this.TryButton.addEventListener('click', () => { + this.handleTryButtonClick(); + }); + + this.AnimationList.addEventListener('change', this.handleAnimationListChange.bind(this)); + } + + setIndex(i){ this.Index = i; } + getIndex(){ return this.Index; } + updateSpeedLabel(){ + if(!this.Speed.hidden){ + this.SpeedLabel.innerHTML = this.SpeedCaption + this.getSpeedValue(); + } + } + updateHueRangeLabel(){ + if(!this.HueRange.hidden){ + this.HueRangeLabel.innerHTML = this.HueRangeCaption + this.getHueRangeValue(); + } + } + updateParam1Label(){ + if(!this.Param1.hidden){ + this.Param1Label.innerHTML = this.Param1Caption + this.getParam1Value(); + } + } + updateParam2Label(){ + if(!this.Param2.hidden){ + this.Param2Label.innerHTML = this.Param2Caption + this.getParam2Value(); + } + } + setTitle(text){ + this.Title.textContent = text; + } + addOptionToList(value, text){ + const optionElement = document.createElement('option'); + optionElement.value = value; + optionElement.textContent = text; + this.AnimationList.appendChild(optionElement); + } + setHidden(hidden){ this.hidden = hidden; } + getHidden(){ return this.hidden; } + setHueValue(value){ this.hueSelect.setHue(value); } + getHueValue(){ return this.hueSelect.getSelectedHue(); } + setSpeedValue(value){ + this.Speed.value = value; + this.updateSpeedLabel(); + } + setSpeedCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.SpeedCaption = caption; + this.SpeedLabel.innerHTML = caption; + this.Speed.hidden = hid; + this.SpeedLabel.hidden = hid; + } + getSpeedValue(){ return this.Speed.value; } + setHueRangeValue(value){ + this.HueRange.value = value; + this.updateHueRangeLabel() + } + setHueRangeCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.HueRangeCaption = caption; + this.HueRangeLabel.innerHTML = caption; + this.HueRange.hidden = hid; + this.HueRangeLabel.hidden = hid; + } + getHueRangeValue(){ return this.HueRange.value; } + setParam1Value(value){ + this.Param1.value = value; + this.updateParam1Label() + } + setParam1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param1Caption = caption; + this.Param1Label.innerHTML = caption; + this.Param1.hidden = hid; + this.Param1Label.hidden = hid; + } + getParam1Value(){ return this.Param1.value; } + setParam2Value(value){ + this.Param2.value = value; + this.updateParam2Label() + } + setParam2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param2Caption = caption; + this.Param2Label.innerHTML = caption; + this.Param2.hidden = hid; + this.Param2Label.hidden = hid; + } + getParam2Value(){ return this.Param2.value; } + + setCheck1Value(value){ this.Check1.checked = value; } + setCheck1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check1Label.innerHTML = caption; + this.Check1.hidden = hid; + this.Check1Label.hidden = hid; + } + getCheck1Value(){ return this.Check1.checked; } + + setCheck2Value(value){ this.Check2.checked = value; } + setCheck2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check2Label.innerHTML = caption; + this.Check2.hidden = hid; + this.Check2Label.hidden = hid; + } + getCheck2Value(){ return this.Check2.checked; } + + setCheck3Value(value){ this.Check3.checked = value; } + setCheck3Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check3Label.innerHTML = caption; + this.Check3.hidden = hid; + this.Check3Label.hidden = hid; + } + getCheck3Value(){ return this.Check3.checked; } + + setCheck4Value(value){ this.Check4.checked = value; } + setCheck4Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check4Label.innerHTML = caption; + this.Check4.hidden = hid; + this.Check4Label.hidden = hid; + } + getCheck4Value(){ return this.Check4.checked; } + + setAnimationIndex(index){ + this.AnimationList.selectedIndex = index; + const changeEvent = new Event('change'); + this.AnimationList.dispatchEvent(changeEvent); + } + getAnimationIndex(){ + return this.AnimationList.selectedIndex; + } + + handleTryButtonClick() { + const eventIndexValue = this.getIndex(); + const animIndexValue = this.getAnimationIndex(); + const hueValue = this.getHueValue(); + const speedValue = this.getSpeedValue(); + const colorRangeValue = this.getHueRangeValue(); + const param1Value = this.getParam1Value(); + const param2Value = this.getParam2Value(); + const check1Value = this.getCheck1Value(); + const check2Value = this.getCheck2Value(); + const check3Value = this.getCheck3Value(); + const check4Value = this.getCheck4Value(); + + this.dispatchEvent(new CustomEvent('tryClick', { + detail: { + eventIndex: eventIndexValue, + animIndex: animIndexValue, + hue: hueValue, + speed: speedValue, + colorRange: colorRangeValue, + param1: param1Value, + param2: param2Value, + check1: check1Value, + check2: check2Value, + check3: check3Value, + check4: check4Value + } + })); + } + + setAnimationCaptions(propsJson){ + this.AnimationPropsJson = propsJson; + + //add options to list + let x = 0; + this.AnimationPropsJson.forEach(props => { + this.addOptionToList(x, props.name); + x++; + }); + + } + + handleAnimationListChange(){ + const selectedIndex = this.getAnimationIndex(); + const selectedProps = this.AnimationPropsJson[selectedIndex]; + + this.updateControlProps(selectedProps); + } + + updateControlProps(props){ + this.setSpeedCaption(props.speed); + let s = props['hue-range']; + this.setHueRangeCaption(s || ""); + this.setParam1Caption(props.param1 || ""); + this.setParam2Caption(props.param2 || ""); + this.setCheck1Caption(props.check1 || ""); + this.setCheck2Caption(props.check2 || ""); + this.setCheck3Caption(props.check3 || ""); + this.setCheck4Caption(props.check4 || ""); + + this.updateSpeedLabel(); + this.updateHueRangeLabel(); + this.updateParam1Label(); + this.updateParam2Label(); + } + +} + +customElements.define('event-box', EventBox); + \ No newline at end of file diff --git a/firmware_update/latest/data/js/fwUoload.js b/firmware_update/latest/data/js/fwUoload.js new file mode 100644 index 0000000..446f3b4 --- /dev/null +++ b/firmware_update/latest/data/js/fwUoload.js @@ -0,0 +1,64 @@ +async function uploadFile(event) { + event.preventDefault(); + + const file = fileInput.files[0]; + if (!file) { + alert("Please select a file."); + return; + } + + // Existing file validation code... + + const formData = new FormData(); + formData.append('file', file); + + // Create a custom reader function + const reader = file.stream().getReader(); + const totalLength = file.size; + + let uploaded = 0; + + // Create a readable stream and use the custom reader function + const uploadStream = new ReadableStream({ + async start(controller) { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + uploaded += value.length; + let percentUploaded = (uploaded / totalLength * 100).toFixed(2); + + // Update progress bar and label + progressBar.value = percentUploaded; + progressLabel.innerHTML = `${percentUploaded}%`; + + controller.enqueue(value); + } + controller.close(); + }, + }); + + // Create a new Request with the readable stream as body + const req = new Request('/update', { + method: 'POST', + body: uploadStream, + headers: { + // Add any relevant headers here + }, + }); + + try { + const response = await fetch(req); + + if (response.ok) { + alert('Upload completed!'); + progressLabel.innerHTML = "Completed!"; + } else { + alert('An error occurred during the upload.'); + progressLabel.innerHTML = "Error!"; + } + } catch (error) { + console.error('Upload failed:', error); + } + } + \ No newline at end of file diff --git a/firmware_update/latest/data/js/hue-select.js b/firmware_update/latest/data/js/hue-select.js new file mode 100644 index 0000000..ca067d1 --- /dev/null +++ b/firmware_update/latest/data/js/hue-select.js @@ -0,0 +1,244 @@ + +class HueSelect extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + + `; + + this.currentHue = 0; + this.colorList; + this.hueLabel = this.shadowRoot.getElementById('hue-label'); + + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + selectedColorDiv.addEventListener('click', () => { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = dropdownContent.style.display === 'block' ? 'none' : 'block'; + }); + + this.createColorOptions(); + this.setHue(0); + } + + generateColors() { + const colors = []; + for (let hue = 0; hue <= 360; hue += 10) { + if (hue == 360) { hue = 359; } + colors.push(hue); + } + + colors.push(-1); + colors.push(-2); + return colors; + } + + createColorOption(hue) { + const colorOption = document.createElement('div'); + colorOption.classList.add('color-option'); + + const colorPatch = document.createElement('span'); + colorPatch.classList.add('color-patch'); + + const colorText = document.createElement('span'); + colorText.classList.add('color-text'); + colorText.textContent = hue; + + const rgbHex = document.createElement('span'); + rgbHex.classList.add('rgb-hex'); + + if (hue === -2) { + colorPatch.style.backgroundColor = 'rgb(0,0,0)'; + rgbHex.innerHTML = '  #000000'; + } else if (hue === -1) { + colorPatch.style.backgroundColor = 'rgb(255,255,255)'; + rgbHex.innerHTML = '  #FFFFFF'; + } else { + colorPatch.style.backgroundColor = `hsl(${hue}, 100%, 50%)`; + const hexColor = this.hslToRgb(hue, 100, 50).toUpperCase(); + rgbHex.innerHTML = `  ${hexColor}`; + } + + colorOption.appendChild(colorPatch); + colorOption.appendChild(colorText); + colorOption.appendChild(rgbHex); + + colorOption.addEventListener('click', () => this.handleColorSelection(hue)); + + return colorOption; + } + + createColorOptions() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + this.colorList = this.generateColors(); + + this.colorList.forEach(hue => { + const colorOption = this.createColorOption(hue); + dropdownContent.appendChild(colorOption); + }); + } + + // Function to convert HSL to RGB + hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + let r, g, b; + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = (x) => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + handleColorSelection(hue) { + this.currentHue = hue; + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + // Update the color patch and hue label + const rgb = this.getRGBfromHue(hue); + colorPatch.style.backgroundColor = rgb; + this.setHueLabel(hue); + this.hideDropdown(); + + this.dispatchEvent(new CustomEvent('change', { detail: { hue, rgb } })); + } + + // Method to get the hue value of the selected item + getSelectedHue() { + return this.currentHue; + } + + getSelectedRGB() { + const selectedColorText = this.shadowRoot.getElementById('selectedColor').textContent.trim(); + return parseFloat(selectedColorText); + } + + getRGBfromHue(hue){ + if(hue == -1){ + return '#FFFFFF'; + }else if(hue == -2){ + return '#000000'; + }else{ + return this.hslToRgb(hue, 100, 50);; + } + } + + // Method to get the RGB value of the selected item + getSelectedRgb() { + const selectedHue = this.getSelectedHue(); + return getRGBfromHue(selectedHue); + } + + setHue(hue) { + // Round the input hue to the nearest 10th + let roundedHue = hue; + if(hue === -1 || hue === -2){ + roundedHue = hue; + }else{ + roundedHue = Math.round(hue / 10) * 10; + if (roundedHue >= 360) { + roundedHue = 359; + }else if (roundedHue < 0) { + roundedHue = 0; + } + } + + this.currentHue = roundedHue; + + this.colorList.forEach(colorVal => { + if (colorVal === roundedHue) { + this.handleColorSelection(roundedHue); + } + }); + + this.hideDropdown(); + } + + hideDropdown() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = 'none'; + } + + setHueLabel(value){ + this.hueLabel.textContent = "Hue: " + value; + } + + + } + + customElements.define('hue-select', HueSelect); + \ No newline at end of file diff --git a/firmware_update/latest/data/js/jquery-3.7.1.js b/firmware_update/latest/data/js/jquery-3.7.1.js new file mode 100644 index 0000000..f7d84ab --- /dev/null +++ b/firmware_update/latest/data/js/jquery-3.7.1.js @@ -0,0 +1,8673 @@ +( function( global, factory ) { + + "use strict"; + + if ( typeof module === "object" && typeof module.exports === "object" ) { + + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +"use strict"; + +var arr = []; + +var getProto = Object.getPrototypeOf; + +var slice = arr.slice; + +var flat = arr.flat ? function( array ) { + return arr.flat.call( array ); +} : function( array ) { + return arr.concat.apply( [], array ); +}; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var fnToString = hasOwn.toString; + +var ObjectFunctionString = fnToString.call( Object ); + +var support = {}; + +var isFunction = function isFunction( obj ) { + + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; + +var isWindow = function isWindow( obj ) { + return obj != null && obj === obj.window; + }; + +var document = window.document; + + var preservedScriptAttributes = { + type: true, + src: true, + nonce: true, + noModule: true + }; + + function DOMEval( code, node, doc ) { + doc = doc || document; + + var i, val, + script = doc.createElement( "script" ); + + script.text = code; + if ( node ) { + for ( i in preservedScriptAttributes ) { + + val = node[ i ] || node.getAttribute && node.getAttribute( i ); + if ( val ) { + script.setAttribute( i, val ); + } + } + } + doc.head.appendChild( script ).parentNode.removeChild( script ); + } + +function toType( obj ) { + if ( obj == null ) { + return obj + ""; + } + + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; +} + +var version = "3.7.1", + + rhtmlSuffix = /HTML$/i, + + jQuery = function( selector, context ) { + + return new jQuery.fn.init( selector, context ); + }; + +jQuery.fn = jQuery.prototype = { + + jquery: version, + + constructor: jQuery, + + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + get: function( num ) { + + if ( num == null ) { + return slice.call( this ); + } + + return num < 0 ? this[ num + this.length ] : this[ num ]; + }, + + pushStack: function( elems ) { + + var ret = jQuery.merge( this.constructor(), elems ); + + ret.prevObject = this; + + return ret; + }, + + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + even: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return ( i + 1 ) % 2; + } ) ); + }, + + odd: function() { + return this.pushStack( jQuery.grep( this, function( _elem, i ) { + return i % 2; + } ) ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + if ( typeof target === "boolean" ) { + deep = target; + + target = arguments[ i ] || {}; + i++; + } + + if ( typeof target !== "object" && !isFunction( target ) ) { + target = {}; + } + + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + if ( ( options = arguments[ i ] ) != null ) { + + for ( name in options ) { + copy = options[ name ]; + + if ( name === "__proto__" || target === copy ) { + continue; + } + + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = Array.isArray( copy ) ) ) ) { + src = target[ name ]; + + if ( copyIsArray && !Array.isArray( src ) ) { + clone = []; + } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { + clone = {}; + } else { + clone = src; + } + copyIsArray = false; + + target[ name ] = jQuery.extend( deep, clone, copy ); + + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + return target; +}; + +jQuery.extend( { + + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isPlainObject: function( obj ) { + var proto, Ctor; + + if ( !obj || toString.call( obj ) !== "[object Object]" ) { + return false; + } + + proto = getProto( obj ); + + if ( !proto ) { + return true; + } + + Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor; + return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString; + }, + + isEmptyObject: function( obj ) { + var name; + + for ( name in obj ) { + return false; + } + return true; + }, + + globalEval: function( code, options, doc ) { + DOMEval( code, { nonce: options && options.nonce }, doc ); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + text: function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + + while ( ( node = elem[ i++ ] ) ) { + + ret += jQuery.text( node ); + } + } + if ( nodeType === 1 || nodeType === 11 ) { + return elem.textContent; + } + if ( nodeType === 9 ) { + return elem.documentElement.textContent; + } + if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + + return ret; + }, + + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + isXMLDoc: function( elem ) { + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; + + return !rhtmlSuffix.test( namespace || docElem && docElem.nodeName || "HTML" ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + return flat( ret ); + }, + + guid: 1, + + support: support +} ); + +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} + +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); + +function isArrayLike( obj ) { + + var length = !!obj && "length" in obj && obj.length, + type = toType( obj ); + + if ( isFunction( obj ) || isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} + +function nodeName( elem, name ) { + + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + +} +var pop = arr.pop; + +var sort = arr.sort; + +var splice = arr.splice; + +var whitespace = "[\\x20\\t\\r\\n\\f]"; + +var rtrimCSS = new RegExp( + "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", + "g" +); + +jQuery.contains = function( a, b ) { + var bup = b && b.parentNode; + + return a === bup || !!( bup && bup.nodeType === 1 && ( + + a.contains ? + a.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + ) ); +}; + +var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; + +function fcssescape( ch, asCodePoint ) { + if ( asCodePoint ) { + + if ( ch === "\0" ) { + return "\uFFFD"; + } + + return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; + } + + return "\\" + ch; +} + +jQuery.escapeSelector = function( sel ) { + return ( sel + "" ).replace( rcssescape, fcssescape ); +}; + +var preferredDoc = document, + pushNative = push; + +( function() { + +var i, + Expr, + outermostContext, + sortInput, + hasDuplicate, + push = pushNative, + + document, + documentElement, + documentIsHTML, + rbuggyQSA, + matches, + + expando = jQuery.expando, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + nonnativeSelectorCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|" + + "loop|multiple|open|readonly|required|scoped", + + identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+", + + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + + "*([*^$|!~]?=)" + whitespace + + + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + + whitespace + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + + ".*" + + ")\\)|)", + + rwhitespace = new RegExp( whitespace + "+", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + + whitespace + "*" ), + rdescend = new RegExp( whitespace + "|>" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + ID: new RegExp( "^#(" + identifier + ")" ), + CLASS: new RegExp( "^\\.(" + identifier + ")" ), + TAG: new RegExp( "^(" + identifier + "|[*])" ), + ATTR: new RegExp( "^" + attributes ), + PSEUDO: new RegExp( "^" + pseudos ), + CHILD: new RegExp( + "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + + whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + + whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + bool: new RegExp( "^(?:" + booleans + ")$", "i" ), + + needsContext: new RegExp( "^" + whitespace + + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace + + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + + runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + + "?|\\\\([^\\r\\n\\f])", "g" ), + funescape = function( escape, nonHex ) { + var high = "0x" + escape.slice( 1 ) - 0x10000; + + if ( nonHex ) { + + return nonHex; + } + + return high < 0 ? + String.fromCharCode( high + 0x10000 ) : + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + unloadHandler = function() { + setDocument(); + }, + + inDisabledFieldset = addCombinator( + function( elem ) { + return elem.disabled === true && nodeName( elem, "fieldset" ); + }, + { dir: "parentNode", next: "legend" } + ); + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +try { + push.apply( + ( arr = slice.call( preferredDoc.childNodes ) ), + preferredDoc.childNodes + ); + + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { + apply: function( target, els ) { + pushNative.apply( target, slice.call( els ) ); + }, + call: function( target ) { + pushNative.apply( target, slice.call( arguments, 1 ) ); + } + }; +} + +function find( selector, context, results, seed ) { + var m, i, elem, nid, match, groups, newSelector, + newContext = context && context.ownerDocument, + + nodeType = context ? context.nodeType : 9; + + results = results || []; + + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + if ( !seed ) { + setDocument( context ); + context = context || document; + + if ( documentIsHTML ) { + + if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) { + + if ( ( m = match[ 1 ] ) ) { + + if ( nodeType === 9 ) { + if ( ( elem = context.getElementById( m ) ) ) { + + if ( elem.id === m ) { + push.call( results, elem ); + return results; + } + } else { + return results; + } + + } else { + + if ( newContext && ( elem = newContext.getElementById( m ) ) && + find.contains( context, elem ) && + elem.id === m ) { + + push.call( results, elem ); + return results; + } + } + + } else if ( match[ 2 ] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + } else if ( ( m = match[ 3 ] ) && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + if ( !nonnativeSelectorCache[ selector + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) { + + newSelector = selector; + newContext = context; + + if ( nodeType === 1 && + ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) { + + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + + if ( newContext != context || !support.scope ) { + + if ( ( nid = context.getAttribute( "id" ) ) ) { + nid = jQuery.escapeSelector( nid ); + } else { + context.setAttribute( "id", ( nid = expando ) ); + } + } + + groups = tokenize( selector ); + i = groups.length; + while ( i-- ) { + groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " + + toSelector( groups[ i ] ); + } + newSelector = groups.join( "," ); + } + + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + nonnativeSelectorCache( selector, true ); + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + + return select( selector.replace( rtrimCSS, "$1" ), context, results, seed ); +} + +function createCache() { + var keys = []; + + function cache( key, value ) { + + if ( keys.push( key + " " ) > Expr.cacheLength ) { + + delete cache[ keys.shift() ]; + } + return ( cache[ key + " " ] = value ); + } + return cache; +} + +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +function assert( fn ) { + var el = document.createElement( "fieldset" ); + + try { + return !!fn( el ); + } catch ( e ) { + return false; + } finally { + + if ( el.parentNode ) { + el.parentNode.removeChild( el ); + } + + el = null; + } +} + +function createInputPseudo( type ) { + return function( elem ) { + return nodeName( elem, "input" ) && elem.type === type; + }; +} + +function createButtonPseudo( type ) { + return function( elem ) { + return ( nodeName( elem, "input" ) || nodeName( elem, "button" ) ) && + elem.type === type; + }; +} + +function createDisabledPseudo( disabled ) { + + return function( elem ) { + + if ( "form" in elem ) { + + if ( elem.parentNode && elem.disabled === false ) { + + if ( "label" in elem ) { + if ( "label" in elem.parentNode ) { + return elem.parentNode.disabled === disabled; + } else { + return elem.disabled === disabled; + } + } + + return elem.isDisabled === disabled || + + elem.isDisabled !== !disabled && + inDisabledFieldset( elem ) === disabled; + } + + return elem.disabled === disabled; + + } else if ( "label" in elem ) { + return elem.disabled === disabled; + } + + return false; + }; +} + +function createPositionalPseudo( fn ) { + return markFunction( function( argument ) { + argument = +argument; + return markFunction( function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + while ( i-- ) { + if ( seed[ ( j = matchIndexes[ i ] ) ] ) { + seed[ j ] = !( matches[ j ] = seed[ j ] ); + } + } + } ); + } ); +} + +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +function setDocument( node ) { + var subWindow, + doc = node ? node.ownerDocument || node : preferredDoc; + + if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + document = doc; + documentElement = document.documentElement; + documentIsHTML = !jQuery.isXMLDoc( document ); + + matches = documentElement.matches || + documentElement.webkitMatchesSelector || + documentElement.msMatchesSelector; + + if ( documentElement.msMatchesSelector && + + preferredDoc != document && + ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) { + + subWindow.addEventListener( "unload", unloadHandler ); + } + + support.getById = assert( function( el ) { + documentElement.appendChild( el ).id = jQuery.expando; + return !document.getElementsByName || + !document.getElementsByName( jQuery.expando ).length; + } ); + + support.disconnectedMatch = assert( function( el ) { + return matches.call( el, "*" ); + } ); + + support.scope = assert( function() { + return document.querySelectorAll( ":scope" ); + } ); + + support.cssHas = assert( function() { + try { + document.querySelector( ":has(*,:jqfake)" ); + return false; + } catch ( e ) { + return true; + } + } ); + + if ( support.getById ) { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute( "id" ) === attrId; + }; + }; + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var elem = context.getElementById( id ); + return elem ? [ elem ] : []; + } + }; + } else { + Expr.filter.ID = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode( "id" ); + return node && node.value === attrId; + }; + }; + + Expr.find.ID = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var node, i, elems, + elem = context.getElementById( id ); + + if ( elem ) { + + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + + elems = context.getElementsByName( id ); + i = 0; + while ( ( elem = elems[ i++ ] ) ) { + node = elem.getAttributeNode( "id" ); + if ( node && node.value === id ) { + return [ elem ]; + } + } + } + + return []; + } + }; + } + + Expr.find.TAG = function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + } else { + return context.querySelectorAll( tag ); + } + }; + + Expr.find.CLASS = function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + rbuggyQSA = []; + + assert( function( el ) { + + var input; + + documentElement.appendChild( el ).innerHTML = + "" + + ""; + + if ( !el.querySelectorAll( "[selected]" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push( "~=" ); + } + + if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push( ".#.+[+~]" ); + } + + if ( !el.querySelectorAll( ":checked" ).length ) { + rbuggyQSA.push( ":checked" ); + } + + input = document.createElement( "input" ); + input.setAttribute( "type", "hidden" ); + el.appendChild( input ).setAttribute( "name", "D" ); + + documentElement.appendChild( el ).disabled = true; + if ( el.querySelectorAll( ":disabled" ).length !== 2 ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + input = document.createElement( "input" ); + input.setAttribute( "name", "" ); + el.appendChild( input ); + if ( !el.querySelectorAll( "[name='']" ).length ) { + rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" + + whitespace + "*(?:''|\"\")" ); + } + } ); + + if ( !support.cssHas ) { + + rbuggyQSA.push( ":has" ); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) ); + + sortOrder = function( a, b ) { + + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + 1; + + if ( compare & 1 || + ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) { + + if ( a === document || a.ownerDocument == preferredDoc && + find.contains( preferredDoc, a ) ) { + return -1; + } + + if ( b === document || b.ownerDocument == preferredDoc && + find.contains( preferredDoc, b ) ) { + return 1; + } + + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + }; + + return document; +} + +find.matches = function( expr, elements ) { + return find( expr, null, null, elements ); +}; + +find.matchesSelector = function( elem, expr ) { + setDocument( elem ); + + if ( documentIsHTML && + !nonnativeSelectorCache[ expr + " " ] && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + if ( ret || support.disconnectedMatch || + + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch ( e ) { + nonnativeSelectorCache( expr, true ); + } + } + + return find( expr, document, null, [ elem ] ).length > 0; +}; + +find.contains = function( context, elem ) { + + if ( ( context.ownerDocument || context ) != document ) { + setDocument( context ); + } + return jQuery.contains( context, elem ); +}; + +find.attr = function( elem, name ) { + + if ( ( elem.ownerDocument || elem ) != document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + if ( val !== undefined ) { + return val; + } + + return elem.getAttribute( name ); +}; + +find.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +jQuery.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + hasDuplicate = !support.sortStable; + sortInput = !support.sortStable && slice.call( results, 0 ); + sort.call( results, sortOrder ); + + if ( hasDuplicate ) { + while ( ( elem = results[ i++ ] ) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + splice.call( results, duplicates[ j ], 1 ); + } + } + + sortInput = null; + + return results; +}; + +jQuery.fn.uniqueSort = function() { + return this.pushStack( jQuery.uniqueSort( slice.apply( this ) ) ); +}; + +Expr = jQuery.expr = { + + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + ATTR: function( match ) { + match[ 1 ] = match[ 1 ].replace( runescape, funescape ); + + match[ 3 ] = ( match[ 3 ] || match[ 4 ] || match[ 5 ] || "" ) + .replace( runescape, funescape ); + + if ( match[ 2 ] === "~=" ) { + match[ 3 ] = " " + match[ 3 ] + " "; + } + + return match.slice( 0, 4 ); + }, + + CHILD: function( match ) { + + match[ 1 ] = match[ 1 ].toLowerCase(); + + if ( match[ 1 ].slice( 0, 3 ) === "nth" ) { + + if ( !match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + match[ 4 ] = +( match[ 4 ] ? + match[ 5 ] + ( match[ 6 ] || 1 ) : + 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) + ); + match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" ); + + } else if ( match[ 3 ] ) { + find.error( match[ 0 ] ); + } + + return match; + }, + + PSEUDO: function( match ) { + var excess, + unquoted = !match[ 6 ] && match[ 2 ]; + + if ( matchExpr.CHILD.test( match[ 0 ] ) ) { + return null; + } + + if ( match[ 3 ] ) { + match[ 2 ] = match[ 4 ] || match[ 5 ] || ""; + + } else if ( unquoted && rpseudo.test( unquoted ) && + + ( excess = tokenize( unquoted, true ) ) && + + ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) { + + match[ 0 ] = match[ 0 ].slice( 0, excess ); + match[ 2 ] = unquoted.slice( 0, excess ); + } + + return match.slice( 0, 3 ); + } + }, + + filter: { + + TAG: function( nodeNameSelector ) { + var expectedNodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { + return true; + } : + function( elem ) { + return nodeName( elem, expectedNodeName ); + }; + }, + + CLASS: function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + ( pattern = new RegExp( "(^|" + whitespace + ")" + className + + "(" + whitespace + "|$)" ) ) && + classCache( className, function( elem ) { + return pattern.test( + typeof elem.className === "string" && elem.className || + typeof elem.getAttribute !== "undefined" && + elem.getAttribute( "class" ) || + "" + ); + } ); + }, + + ATTR: function( name, operator, check ) { + return function( elem ) { + var result = find.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + if ( operator === "=" ) { + return result === check; + } + if ( operator === "!=" ) { + return result !== check; + } + if ( operator === "^=" ) { + return check && result.indexOf( check ) === 0; + } + if ( operator === "*=" ) { + return check && result.indexOf( check ) > -1; + } + if ( operator === "$=" ) { + return check && result.slice( -check.length ) === check; + } + if ( operator === "~=" ) { + return ( " " + result.replace( rwhitespace, " " ) + " " ) + .indexOf( check ) > -1; + } + if ( operator === "|=" ) { + return result === check || result.slice( 0, check.length + 1 ) === check + "-"; + } + + return false; + }; + }, + + CHILD: function( type, what, _argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, _context, xml ) { + var cache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + if ( simple ) { + while ( dir ) { + node = elem; + while ( ( node = node[ dir ] ) ) { + if ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) { + + return false; + } + } + + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + if ( forward && useCache ) { + + outerCache = parent[ expando ] || ( parent[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( ( node = ++nodeIndex && node && node[ dir ] || + + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + + if ( useCache ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + cache = outerCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + if ( diff === false ) { + + while ( ( node = ++nodeIndex && node && node[ dir ] || + ( diff = nodeIndex = 0 ) || start.pop() ) ) { + + if ( ( ofType ? + nodeName( node, name ) : + node.nodeType === 1 ) && + ++diff ) { + + if ( useCache ) { + outerCache = node[ expando ] || + ( node[ expando ] = {} ); + outerCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + PSEUDO: function( pseudo, argument ) { + + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + find.error( "unsupported pseudo: " + pseudo ); + + if ( fn[ expando ] ) { + return fn( argument ); + } + + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction( function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[ i ] ); + seed[ idx ] = !( matches[ idx ] = matched[ i ] ); + } + } ) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + + not: markFunction( function( selector ) { + + var input = [], + results = [], + matcher = compile( selector.replace( rtrimCSS, "$1" ) ); + + return matcher[ expando ] ? + markFunction( function( seed, matches, _context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + while ( i-- ) { + if ( ( elem = unmatched[ i ] ) ) { + seed[ i ] = !( matches[ i ] = elem ); + } + } + } ) : + function( elem, _context, xml ) { + input[ 0 ] = elem; + matcher( input, null, xml, results ); + + input[ 0 ] = null; + return !results.pop(); + }; + } ), + + has: markFunction( function( selector ) { + return function( elem ) { + return find( selector, elem ).length > 0; + }; + } ), + + contains: markFunction( function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || jQuery.text( elem ) ).indexOf( text ) > -1; + }; + } ), + + lang: markFunction( function( lang ) { + + if ( !ridentifier.test( lang || "" ) ) { + find.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( ( elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 ); + return false; + }; + } ), + + target: function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + root: function( elem ) { + return elem === documentElement; + }, + + focus: function( elem ) { + return elem === safeActiveElement() && + document.hasFocus() && + !!( elem.type || elem.href || ~elem.tabIndex ); + }, + + enabled: createDisabledPseudo( false ), + disabled: createDisabledPseudo( true ), + + checked: function( elem ) { + + return ( nodeName( elem, "input" ) && !!elem.checked ) || + ( nodeName( elem, "option" ) && !!elem.selected ); + }, + + selected: function( elem ) { + + if ( elem.parentNode ) { + + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + empty: function( elem ) { + + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + parent: function( elem ) { + return !Expr.pseudos.empty( elem ); + }, + + header: function( elem ) { + return rheader.test( elem.nodeName ); + }, + + input: function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + button: function( elem ) { + return nodeName( elem, "input" ) && elem.type === "button" || + nodeName( elem, "button" ); + }, + + text: function( elem ) { + var attr; + return nodeName( elem, "input" ) && elem.type === "text" && + + ( ( attr = elem.getAttribute( "type" ) ) == null || + attr.toLowerCase() === "text" ); + }, + + first: createPositionalPseudo( function() { + return [ 0 ]; + } ), + + last: createPositionalPseudo( function( _matchIndexes, length ) { + return [ length - 1 ]; + } ), + + eq: createPositionalPseudo( function( _matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + } ), + + even: createPositionalPseudo( function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + odd: createPositionalPseudo( function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + lt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i; + + if ( argument < 0 ) { + i = argument + length; + } else if ( argument > length ) { + i = length; + } else { + i = argument; + } + + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ), + + gt: createPositionalPseudo( function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + } ) + } +}; + +Expr.pseudos.nth = Expr.pseudos.eq; + +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +function tokenize( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + if ( !matched || ( match = rcomma.exec( soFar ) ) ) { + if ( match ) { + + soFar = soFar.slice( match[ 0 ].length ) || soFar; + } + groups.push( ( tokens = [] ) ); + } + + matched = false; + + if ( ( match = rleadingCombinator.exec( soFar ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + + type: match[ 0 ].replace( rtrimCSS, " " ) + } ); + soFar = soFar.slice( matched.length ); + } + + for ( type in Expr.filter ) { + if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] || + ( match = preFilters[ type ]( match ) ) ) ) { + matched = match.shift(); + tokens.push( { + value: matched, + type: type, + matches: match + } ); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + if ( parseOnly ) { + return soFar.length; + } + + return soFar ? + find.error( selector ) : + + tokenCache( selector, groups ).slice( 0 ); +} + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[ i ].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + skip = combinator.next, + key = skip || dir, + checkNonElements = base && key === "parentNode", + doneName = done++; + + return combinator.first ? + + function( elem, context, xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + return false; + } : + + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + if ( xml ) { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( ( elem = elem[ dir ] ) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || ( elem[ expando ] = {} ); + + if ( skip && nodeName( elem, skip ) ) { + elem = elem[ dir ] || elem; + } else if ( ( oldCache = outerCache[ key ] ) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + return ( newCache[ 2 ] = oldCache[ 2 ] ); + } else { + + outerCache[ key ] = newCache; + + if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) { + return true; + } + } + } + } + } + return false; + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[ i ]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[ 0 ]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + find( selector, contexts[ i ], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( ( elem = unmatched[ i ] ) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction( function( seed, results, context, xml ) { + var temp, i, elem, matcherOut, + preMap = [], + postMap = [], + preexisting = results.length, + + elems = seed || + multipleContexts( selector || "*", + context.nodeType ? [ context ] : context, [] ), + + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems; + + if ( matcher ) { + + matcherOut = postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + [] : + + results; + + matcher( matcherIn, matcherOut, context, xml ); + } else { + matcherOut = matcherIn; + } + + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + i = temp.length; + while ( i-- ) { + if ( ( elem = temp[ i ] ) ) { + matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem ); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) ) { + + temp.push( ( matcherIn[ i ] = elem ) ); + } + } + postFinder( null, ( matcherOut = [] ), temp, xml ); + } + + i = matcherOut.length; + while ( i-- ) { + if ( ( elem = matcherOut[ i ] ) && + ( temp = postFinder ? indexOf.call( seed, elem ) : preMap[ i ] ) > -1 ) { + + seed[ temp ] = !( results[ temp ] = elem ); + } + } + } + + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + } ); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[ 0 ].type ], + implicitRelative = leadingRelative || Expr.relative[ " " ], + i = leadingRelative ? 1 : 0, + + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + + var ret = ( !leadingRelative && ( xml || context != outermostContext ) ) || ( + ( checkContext = context ).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) { + matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ]; + } else { + matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches ); + + if ( matcher[ expando ] ) { + + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[ j ].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + + tokens.slice( 0, i - 1 ) + .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } ) + ).replace( rtrimCSS, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + + elems = seed || byElement && Expr.find.TAG( "*", outermost ), + + dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ), + len = elems.length; + + if ( outermost ) { + + outermostContext = context == document || context || outermost; + } + + for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + + if ( !context && elem.ownerDocument != document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( ( matcher = elementMatchers[ j++ ] ) ) { + if ( matcher( elem, context || document, xml ) ) { + push.call( results, elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + if ( bySet ) { + + if ( ( elem = !matcher && elem ) ) { + matchedCount--; + } + + if ( seed ) { + unmatched.push( elem ); + } + } + } + + matchedCount += i; + + if ( bySet && i !== matchedCount ) { + j = 0; + while ( ( matcher = setMatchers[ j++ ] ) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !( unmatched[ i ] || setMatched[ i ] ) ) { + setMatched[ i ] = pop.call( results ); + } + } + } + + setMatched = condense( setMatched ); + } + + push.apply( results, setMatched ); + + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + jQuery.uniqueSort( results ); + } + } + + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +function compile( selector, match ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[ i ] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + cached = compilerCache( selector, + matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + cached.selector = selector; + } + return cached; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( ( selector = compiled.selector || selector ) ); + + results = results || []; + + if ( match.length === 1 ) { + + tokens = match[ 0 ] = match[ 0 ].slice( 0 ); + if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" && + context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) { + + context = ( Expr.find.ID( + token.matches[ 0 ].replace( runescape, funescape ), + context + ) || [] )[ 0 ]; + if ( !context ) { + return results; + + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + i = matchExpr.needsContext.test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[ i ]; + + if ( Expr.relative[ ( type = token.type ) ] ) { + break; + } + if ( ( find = Expr.find[ type ] ) ) { + + if ( ( seed = find( + token.matches[ 0 ].replace( runescape, funescape ), + rsibling.test( tokens[ 0 ].type ) && + testContext( context.parentNode ) || context + ) ) ) { + + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +} + +support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando; + +setDocument(); + +support.sortDetached = assert( function( el ) { + + return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1; +} ); + +jQuery.find = find; + +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.unique = jQuery.uniqueSort; + +find.compile = compile; +find.select = select; +find.setDocument = setDocument; +find.tokenize = tokenize; + +find.escape = jQuery.escapeSelector; +find.getText = jQuery.text; +find.isXML = jQuery.isXMLDoc; +find.selectors = jQuery.expr; +find.support = jQuery.support; +find.uniqueSort = jQuery.uniqueSort; + +} )(); + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); + +function winnow( elements, qualifier, not ) { + if ( isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + return !!qualifier.call( elem, i, elem ) !== not; + } ); + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + } + + if ( typeof qualifier !== "string" ) { + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); + } + + return jQuery.filter( qualifier, elements, not ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + if ( elems.length === 1 && elem.nodeType === 1 ) { + return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : []; + } + + return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, ret, + len = this.length, + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + ret = this.pushStack( [] ); + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + return len > 1 ? jQuery.uniqueSort( ret ) : ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + +var rootjQuery, + + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + if ( !selector ) { + return this; + } + + root = root || rootjQuery; + + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + if ( match && ( match[ 1 ] || !context ) ) { + + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + if ( isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + } else { + elem = document.getElementById( match[ 2 ] ); + + if ( elem ) { + + this[ 0 ] = elem; + this.length = 1; + } + return this; + } + + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + } else { + return this.constructor( context ).find( selector ); + } + + } else if ( selector.nodeType ) { + this[ 0 ] = selector; + this.length = 1; + return this; + + } else if ( isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + selector( jQuery ); + } + + return jQuery.makeArray( selector, this ); + }; + +init.prototype = jQuery.fn; + +rootjQuery = jQuery( document ); + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + targets = typeof selectors !== "string" && jQuery( selectors ); + + if ( !rneedsContext.test( selectors ) ) { + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + if ( cur.nodeType < 11 && ( targets ? + targets.index( cur ) > -1 : + + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + index: function( elem ) { + + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + return indexOf.call( this, + + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, _i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, _i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, _i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + if ( elem.contentDocument != null && + + getProto( elem.contentDocument ) ) { + + return elem.contentDocument; + } + + if ( nodeName( elem, "template" ) ) { + elem = elem.content || elem; + } + + return jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnothtmlwhite = ( /[^\x20\t\r\n\f]+/g ); + +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnothtmlwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +jQuery.Callbacks = function( options ) { + + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var + firing, + + memory, + + fired, + + locked, + + list = [], + + queue = [], + + firingIndex = -1, + + fire = function() { + + locked = locked || options.once; + + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + firingIndex = list.length; + memory = false; + } + } + } + + if ( !options.memory ) { + memory = false; + } + + firing = false; + + if ( locked ) { + + if ( memory ) { + list = []; + + } else { + list = ""; + } + } + }, + + self = { + + add: function() { + if ( list ) { + + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && toType( arg ) !== "string" ) { + + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + lock: function() { + locked = queue = []; + if ( !memory && !firing ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + fired: function() { + return !!fired; + } + }; + + return self; +}; + +function Identity( v ) { + return v; +} +function Thrower( ex ) { + throw ex; +} + +function adoptValue( value, resolve, reject, noValue ) { + var method; + + try { + + if ( value && isFunction( ( method = value.promise ) ) ) { + method.call( value ).done( resolve ).fail( reject ); + + } else if ( value && isFunction( ( method = value.then ) ) ) { + method.call( value, resolve, reject ); + + } else { + + resolve.apply( undefined, [ value ].slice( noValue ) ); + } + + } catch ( value ) { + + reject.apply( undefined, [ value ] ); + } +} + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + [ "notify", "progress", jQuery.Callbacks( "memory" ), + jQuery.Callbacks( "memory" ), 2 ], + [ "resolve", "done", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 0, "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), + jQuery.Callbacks( "once memory" ), 1, "rejected" ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + "catch": function( fn ) { + return promise.then( null, fn ); + }, + + pipe: function( ) { + var fns = arguments; + + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( _i, tuple ) { + + var fn = isFunction( fns[ tuple[ 4 ] ] ) && fns[ tuple[ 4 ] ]; + + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + then: function( onFulfilled, onRejected, onProgress ) { + var maxDepth = 0; + function resolve( depth, deferred, handler, special ) { + return function() { + var that = this, + args = arguments, + mightThrow = function() { + var returned, then; + + if ( depth < maxDepth ) { + return; + } + + returned = handler.apply( that, args ); + + if ( returned === deferred.promise() ) { + throw new TypeError( "Thenable self-resolution" ); + } + + then = returned && + + ( typeof returned === "object" || + typeof returned === "function" ) && + returned.then; + + if ( isFunction( then ) ) { + + if ( special ) { + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ) + ); + + } else { + + maxDepth++; + + then.call( + returned, + resolve( maxDepth, deferred, Identity, special ), + resolve( maxDepth, deferred, Thrower, special ), + resolve( maxDepth, deferred, Identity, + deferred.notifyWith ) + ); + } + + } else { + + if ( handler !== Identity ) { + that = undefined; + args = [ returned ]; + } + + ( special || deferred.resolveWith )( that, args ); + } + }, + + process = special ? + mightThrow : + function() { + try { + mightThrow(); + } catch ( e ) { + + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.error ); + } + + if ( depth + 1 >= maxDepth ) { + + if ( handler !== Thrower ) { + that = undefined; + args = [ e ]; + } + + deferred.rejectWith( that, args ); + } + } + }; + + if ( depth ) { + process(); + } else { + + if ( jQuery.Deferred.getErrorHook ) { + process.error = jQuery.Deferred.getErrorHook(); + + } else if ( jQuery.Deferred.getStackHook ) { + process.error = jQuery.Deferred.getStackHook(); + } + window.setTimeout( process ); + } + }; + } + + return jQuery.Deferred( function( newDefer ) { + + tuples[ 0 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onProgress ) ? + onProgress : + Identity, + newDefer.notifyWith + ) + ); + + tuples[ 1 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onFulfilled ) ? + onFulfilled : + Identity + ) + ); + + tuples[ 2 ][ 3 ].add( + resolve( + 0, + newDefer, + isFunction( onRejected ) ? + onRejected : + Thrower + ) + ); + } ).promise(); + }, + + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 5 ]; + + promise[ tuple[ 1 ] ] = list.add; + + if ( stateString ) { + list.add( + function() { + + state = stateString; + }, + + tuples[ 3 - i ][ 2 ].disable, + + tuples[ 3 - i ][ 3 ].disable, + + tuples[ 0 ][ 2 ].lock, + + tuples[ 0 ][ 3 ].lock + ); + } + + list.add( tuple[ 3 ].fire ); + + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? undefined : this, arguments ); + return this; + }; + + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + promise.promise( deferred ); + + if ( func ) { + func.call( deferred, deferred ); + } + + return deferred; + }, + + when: function( singleValue ) { + var + + remaining = arguments.length, + + i = remaining, + + resolveContexts = Array( i ), + resolveValues = slice.call( arguments ), + + primary = jQuery.Deferred(), + + updateFunc = function( i ) { + return function( value ) { + resolveContexts[ i ] = this; + resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( !( --remaining ) ) { + primary.resolveWith( resolveContexts, resolveValues ); + } + }; + }; + + if ( remaining <= 1 ) { + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, + !remaining ); + + if ( primary.state() === "pending" || + isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { + + return primary.then(); + } + } + + while ( i-- ) { + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); + } + + return primary.promise(); + } +} ); + +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, asyncError ) { + + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, + error.stack, asyncError ); + } +}; + +jQuery.readyException = function( error ) { + window.setTimeout( function() { + throw error; + } ); +}; + +var readyList = jQuery.Deferred(); + +jQuery.fn.ready = function( fn ) { + + readyList + .then( fn ) + + .catch( function( error ) { + jQuery.readyException( error ); + } ); + + return this; +}; + +jQuery.extend( { + + isReady: false, + + readyWait: 1, + + ready: function( wait ) { + + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + jQuery.isReady = true; + + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + readyList.resolveWith( document, [ jQuery ] ); + } +} ); + +jQuery.ready.then = readyList.then; + +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + window.setTimeout( jQuery.ready ); + +} else { + + document.addEventListener( "DOMContentLoaded", completed ); + + window.addEventListener( "load", completed ); +} + +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + if ( toType( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + } else if ( value !== undefined ) { + chainable = true; + + if ( !isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + if ( raw ) { + fn.call( elems, value ); + fn = null; + + } else { + bulk = fn; + fn = function( elem, _key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + if ( chainable ) { + return elems; + } + + if ( bulk ) { + return fn.call( elems ); + } + + return len ? fn( elems[ 0 ], key ) : emptyGet; +}; + +var rmsPrefix = /^-ms-/, + rdashAlpha = /-([a-z])/g; + +function fcamelCase( _all, letter ) { + return letter.toUpperCase(); +} + +function camelCase( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); +} +var acceptData = function( owner ) { + + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + cache: function( owner ) { + + var value = owner[ this.expando ]; + + if ( !value ) { + value = {}; + + if ( acceptData( owner ) ) { + + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + if ( typeof data === "string" ) { + cache[ camelCase( data ) ] = value; + + } else { + + for ( prop in data ) { + cache[ camelCase( prop ) ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + + owner[ this.expando ] && owner[ this.expando ][ camelCase( key ) ]; + }, + access: function( owner, key, value ) { + + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + return this.get( owner, key ); + } + + this.set( owner, key, value ); + + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key !== undefined ) { + + if ( Array.isArray( key ) ) { + + key = key.map( camelCase ); + } else { + key = camelCase( key ); + + key = key in cache ? + [ key ] : + ( key.match( rnothtmlwhite ) || [] ); + } + + i = key.length; + + while ( i-- ) { + delete cache[ key[ i ] ]; + } + } + + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function getData( data ) { + if ( data === "true" ) { + return true; + } + + if ( data === "false" ) { + return false; + } + + if ( data === "null" ) { + return null; + } + + if ( data === +data + "" ) { + return +data; + } + + if ( rbrace.test( data ) ) { + return JSON.parse( data ); + } + + return data; +} + +function dataAttr( elem, key, data ) { + var name; + + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = getData( data ); + } catch ( e ) {} + + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data; + + if ( elem && value === undefined ) { + + data = dataUser.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + data = dataAttr( elem, key ); + if ( data !== undefined ) { + return data; + } + + return; + } + + this.each( function() { + + dataUser.set( this, key, value ); + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + if ( data ) { + if ( !queue || Array.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var documentElement = document.documentElement; + + var isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ); + }, + composed = { composed: true }; + + if ( documentElement.getRootNode ) { + isAttached = function( elem ) { + return jQuery.contains( elem.ownerDocument, elem ) || + elem.getRootNode( composed ) === elem.ownerDocument; + }; + } +var isHiddenWithinTree = function( elem, el ) { + + elem = el || elem; + + return elem.style.display === "none" || + elem.style.display === "" && + + isAttached( elem ) && + + jQuery.css( elem, "display" ) === "none"; + }; + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, scale, + maxIterations = 20, + currentValue = tween ? + function() { + return tween.cur(); + } : + function() { + return jQuery.css( elem, prop, "" ); + }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + initialInUnit = elem.nodeType && + ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + initial = initial / 2; + + unit = unit || initialInUnit[ 3 ]; + + initialInUnit = +initial || 1; + + while ( maxIterations-- ) { + + jQuery.style( elem, prop, initialInUnit + unit ); + if ( ( 1 - scale ) * ( 1 - ( scale = currentValue() / initial || 0.5 ) ) <= 0 ) { + maxIterations = 0; + } + initialInUnit = initialInUnit / scale; + + } + + initialInUnit = initialInUnit * 2; + jQuery.style( elem, prop, initialInUnit + unit ); + + valueParts = valueParts || []; + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} + +var defaultDisplayMap = {}; + +function getDefaultDisplay( elem ) { + var temp, + doc = elem.ownerDocument, + nodeName = elem.nodeName, + display = defaultDisplayMap[ nodeName ]; + + if ( display ) { + return display; + } + + temp = doc.body.appendChild( doc.createElement( nodeName ) ); + display = jQuery.css( temp, "display" ); + + temp.parentNode.removeChild( temp ); + + if ( display === "none" ) { + display = "block"; + } + defaultDisplayMap[ nodeName ] = display; + + return display; +} + +function showHide( elements, show ) { + var display, elem, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + display = elem.style.display; + if ( show ) { + + if ( display === "none" ) { + values[ index ] = dataPriv.get( elem, "display" ) || null; + if ( !values[ index ] ) { + elem.style.display = ""; + } + } + if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) { + values[ index ] = getDefaultDisplay( elem ); + } + } else { + if ( display !== "none" ) { + values[ index ] = "none"; + + dataPriv.set( elem, "display", display ); + } + } + } + + for ( index = 0; index < length; index++ ) { + if ( values[ index ] != null ) { + elements[ index ].style.display = values[ index ]; + } + } + + return elements; +} + +jQuery.fn.extend( { + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each( function() { + if ( isHiddenWithinTree( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + } ); + } +} ); +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([a-z][^\/\0>\x20\t\r\n\f]*)/i ); + +var rscriptType = ( /^$|^module$|\/(?:java|ecma)script/i ); + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; + + div.innerHTML = ""; + support.option = !!div.lastChild; +} )(); + +var wrapMap = { + + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +if ( !support.option ) { + wrapMap.optgroup = wrapMap.option = [ 1, "" ]; +} + +function getAll( context, tag ) { + + var ret; + + if ( typeof context.getElementsByTagName !== "undefined" ) { + ret = context.getElementsByTagName( tag || "*" ); + + } else if ( typeof context.querySelectorAll !== "undefined" ) { + ret = context.querySelectorAll( tag || "*" ); + + } else { + ret = []; + } + + if ( tag === undefined || tag && nodeName( context, tag ) ) { + return jQuery.merge( [ context ], ret ); + } + + return ret; +} + +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, attached, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + if ( toType( elem ) === "object" ) { + + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + jQuery.merge( nodes, tmp.childNodes ); + + tmp = fragment.firstChild; + + tmp.textContent = ""; + } + } + } + + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + attached = isAttached( elem ); + + tmp = getAll( fragment.appendChild( elem ), "script" ); + + if ( attached ) { + setGlobalEval( tmp ); + } + + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + if ( typeof types === "object" ) { + + if ( typeof selector !== "string" ) { + + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + fn = data; + data = undefined; + } else { + + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + if ( !acceptData( elem ) ) { + return; + } + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + if ( selector ) { + jQuery.find.matchesSelector( documentElement, selector ); + } + + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + if ( !( events = elemData.events ) ) { + events = elemData.events = Object.create( null ); + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + if ( !type ) { + continue; + } + + special = jQuery.event.special[ type ] || {}; + + type = ( selector ? special.delegateType : special.bindType ) || type; + + special = jQuery.event.special[ type ] || {}; + + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + jQuery.event.global[ type ] = true; + } + + }, + + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + types = ( types || "" ).match( rnothtmlwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( nativeEvent ) { + + var i, j, ret, matched, handleObj, handlerQueue, + args = new Array( arguments.length ), + + event = jQuery.event.fix( nativeEvent ), + + handlers = ( + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + args[ 0 ] = event; + + for ( i = 1; i < arguments.length; i++ ) { + args[ i ] = arguments[ i ]; + } + + event.delegateTarget = this; + + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + if ( !event.rnamespace || handleObj.namespace === false || + event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, handleObj, sel, matchedHandlers, matchedSelectors, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + if ( delegateCount && + + cur.nodeType && + + !( event.type === "click" && event.button >= 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + if ( cur.nodeType === 1 && !( event.type === "click" && cur.disabled === true ) ) { + matchedHandlers = []; + matchedSelectors = {}; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + sel = handleObj.selector + " "; + + if ( matchedSelectors[ sel ] === undefined ) { + matchedSelectors[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matchedSelectors[ sel ] ) { + matchedHandlers.push( handleObj ); + } + } + if ( matchedHandlers.length ) { + handlerQueue.push( { elem: cur, handlers: matchedHandlers } ); + } + } + } + } + + cur = this; + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: cur, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + addProp: function( name, hook ) { + Object.defineProperty( jQuery.Event.prototype, name, { + enumerable: true, + configurable: true, + + get: isFunction( hook ) ? + function() { + if ( this.originalEvent ) { + return hook( this.originalEvent ); + } + } : + function() { + if ( this.originalEvent ) { + return this.originalEvent[ name ]; + } + }, + + set: function( value ) { + Object.defineProperty( this, name, { + enumerable: true, + configurable: true, + writable: true, + value: value + } ); + } + } ); + }, + + fix: function( originalEvent ) { + return originalEvent[ jQuery.expando ] ? + originalEvent : + new jQuery.Event( originalEvent ); + }, + + special: { + load: { + + noBubble: true + }, + click: { + + setup: function( data ) { + + var el = this || data; + + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click", true ); + } + + return false; + }, + trigger: function( data ) { + + var el = this || data; + + if ( rcheckableType.test( el.type ) && + el.click && nodeName( el, "input" ) ) { + + leverageNative( el, "click" ); + } + + return true; + }, + + _default: function( event ) { + var target = event.target; + return rcheckableType.test( target.type ) && + target.click && nodeName( target, "input" ) && + dataPriv.get( target, "click" ) || + nodeName( target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +function leverageNative( el, type, isSetup ) { + + if ( !isSetup ) { + if ( dataPriv.get( el, type ) === undefined ) { + jQuery.event.add( el, type, returnTrue ); + } + return; + } + + dataPriv.set( el, type, false ); + jQuery.event.add( el, type, { + namespace: false, + handler: function( event ) { + var result, + saved = dataPriv.get( this, type ); + + if ( ( event.isTrigger & 1 ) && this[ type ] ) { + + if ( !saved ) { + + saved = slice.call( arguments ); + dataPriv.set( this, type, saved ); + + this[ type ](); + result = dataPriv.get( this, type ); + dataPriv.set( this, type, false ); + + if ( saved !== result ) { + + event.stopImmediatePropagation(); + event.preventDefault(); + + return result; + } + + } else if ( ( jQuery.event.special[ type ] || {} ).delegateType ) { + event.stopPropagation(); + } + + } else if ( saved ) { + + dataPriv.set( this, type, jQuery.event.trigger( + saved[ 0 ], + saved.slice( 1 ), + this + ) ); + + event.stopPropagation(); + event.isImmediatePropagationStopped = returnTrue; + } + } + } ); +} + +jQuery.removeEvent = function( elem, type, handle ) { + + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + src.returnValue === false ? + returnTrue : + returnFalse; + + this.target = ( src.target && src.target.nodeType === 3 ) ? + src.target.parentNode : + src.target; + + this.currentTarget = src.currentTarget; + this.relatedTarget = src.relatedTarget; + + } else { + this.type = src; + } + + if ( props ) { + jQuery.extend( this, props ); + } + + this.timeStamp = src && src.timeStamp || Date.now(); + + this[ jQuery.expando ] = true; +}; + +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +jQuery.each( { + altKey: true, + bubbles: true, + cancelable: true, + changedTouches: true, + ctrlKey: true, + detail: true, + eventPhase: true, + metaKey: true, + pageX: true, + pageY: true, + shiftKey: true, + view: true, + "char": true, + code: true, + charCode: true, + key: true, + keyCode: true, + button: true, + buttons: true, + clientX: true, + clientY: true, + offsetX: true, + offsetY: true, + pointerId: true, + pointerType: true, + screenX: true, + screenY: true, + targetTouches: true, + toElement: true, + touches: true, + which: true +}, jQuery.event.addProp ); + +jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { + + function focusMappedHandler( nativeEvent ) { + if ( document.documentMode ) { + + var handle = dataPriv.get( this, "handle" ), + event = jQuery.event.fix( nativeEvent ); + event.type = nativeEvent.type === "focusin" ? "focus" : "blur"; + event.isSimulated = true; + + handle( nativeEvent ); + + if ( event.target === event.currentTarget ) { + + handle( event ); + } + } else { + + jQuery.event.simulate( delegateType, nativeEvent.target, + jQuery.event.fix( nativeEvent ) ); + } + } + + jQuery.event.special[ type ] = { + + setup: function() { + + var attaches; + + leverageNative( this, type, true ); + + if ( document.documentMode ) { + + attaches = dataPriv.get( this, delegateType ); + if ( !attaches ) { + this.addEventListener( delegateType, focusMappedHandler ); + } + dataPriv.set( this, delegateType, ( attaches || 0 ) + 1 ); + } else { + + return false; + } + }, + trigger: function() { + + leverageNative( this, type ); + + return true; + }, + + teardown: function() { + var attaches; + + if ( document.documentMode ) { + attaches = dataPriv.get( this, delegateType ) - 1; + if ( !attaches ) { + this.removeEventListener( delegateType, focusMappedHandler ); + dataPriv.remove( this, delegateType ); + } else { + dataPriv.set( this, delegateType, attaches ); + } + } else { + + return false; + } + }, + + _default: function( event ) { + return dataPriv.get( event.target, type ); + }, + + delegateType: delegateType + }; + + jQuery.event.special[ delegateType ] = { + setup: function() { + + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ); + + if ( !attaches ) { + if ( document.documentMode ) { + this.addEventListener( delegateType, focusMappedHandler ); + } else { + doc.addEventListener( type, focusMappedHandler, true ); + } + } + dataPriv.set( dataHolder, delegateType, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this.document || this, + dataHolder = document.documentMode ? this : doc, + attaches = dataPriv.get( dataHolder, delegateType ) - 1; + + if ( !attaches ) { + if ( document.documentMode ) { + this.removeEventListener( delegateType, focusMappedHandler ); + } else { + doc.removeEventListener( type, focusMappedHandler, true ); + } + dataPriv.remove( dataHolder, delegateType ); + } else { + dataPriv.set( dataHolder, delegateType, attaches ); + } + } + }; +} ); + +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + +var + + rnoInnerhtml = /\s*$/g; + +function manipulationTarget( elem, content ) { + if ( nodeName( elem, "table" ) && + nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ) { + + return jQuery( elem ).children( "tbody" )[ 0 ] || elem; + } + + return elem; +} + +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + if ( ( elem.type || "" ).slice( 0, 5 ) === "true/" ) { + elem.type = elem.type.slice( 5 ); + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.get( src ); + events = pdataOld.events; + + if ( events ) { + dataPriv.remove( dest, "handle events" ); + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + args = flat( args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + valueIsFunction = isFunction( value ); + + if ( valueIsFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( valueIsFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + if ( hasScripts ) { + + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + jQuery.map( scripts, restoreScript ); + + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src && ( node.type || "" ).toLowerCase() !== "module" ) { + + if ( jQuery._evalUrl && !node.noModule ) { + jQuery._evalUrl( node.src, { + nonce: node.nonce || node.getAttribute( "nonce" ) + }, doc ); + } + } else { + + DOMEval( node.textContent.replace( rcleanScript, "" ), node, doc ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && isAttached( node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html; + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = isAttached( elem ); + + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + jQuery.cleanData( getAll( elem, false ) ); + + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); +var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); + +var rcustomProp = /^--/; + +var getStyles = function( elem ) { + + var view = elem.ownerDocument.defaultView; + + if ( !view || !view.opener ) { + view = window; + } + + return view.getComputedStyle( elem ); + }; + +var swap = function( elem, options, callback ) { + var ret, name, + old = {}; + + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.call( elem ); + + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; +}; + +var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); + +( function() { + + function computeStyleTests() { + + if ( !div ) { + return; + } + + container.style.cssText = "position:absolute;left:-11111px;width:60px;" + + "margin-top:1px;padding:0;border:0"; + div.style.cssText = + "position:relative;display:block;box-sizing:border-box;overflow:scroll;" + + "margin:auto;border:1px;padding:1px;" + + "width:60%;top:1%"; + documentElement.appendChild( container ).appendChild( div ); + + var divStyle = window.getComputedStyle( div ); + pixelPositionVal = divStyle.top !== "1%"; + + reliableMarginLeftVal = roundPixelMeasures( divStyle.marginLeft ) === 12; + + div.style.right = "60%"; + pixelBoxStylesVal = roundPixelMeasures( divStyle.right ) === 36; + + boxSizingReliableVal = roundPixelMeasures( divStyle.width ) === 36; + + div.style.position = "absolute"; + scrollboxSizeVal = roundPixelMeasures( div.offsetWidth / 3 ) === 12; + + documentElement.removeChild( container ); + + div = null; + } + + function roundPixelMeasures( measure ) { + return Math.round( parseFloat( measure ) ); + } + + var pixelPositionVal, boxSizingReliableVal, scrollboxSizeVal, pixelBoxStylesVal, + reliableTrDimensionsVal, reliableMarginLeftVal, + container = document.createElement( "div" ), + div = document.createElement( "div" ); + + if ( !div.style ) { + return; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + jQuery.extend( support, { + boxSizingReliable: function() { + computeStyleTests(); + return boxSizingReliableVal; + }, + pixelBoxStyles: function() { + computeStyleTests(); + return pixelBoxStylesVal; + }, + pixelPosition: function() { + computeStyleTests(); + return pixelPositionVal; + }, + reliableMarginLeft: function() { + computeStyleTests(); + return reliableMarginLeftVal; + }, + scrollboxSize: function() { + computeStyleTests(); + return scrollboxSizeVal; + }, + + reliableTrDimensions: function() { + var table, tr, trChild, trStyle; + if ( reliableTrDimensionsVal == null ) { + table = document.createElement( "table" ); + tr = document.createElement( "tr" ); + trChild = document.createElement( "div" ); + + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "box-sizing:content-box;border:1px solid"; + + tr.style.height = "1px"; + trChild.style.height = "9px"; + + trChild.style.display = "block"; + + documentElement + .appendChild( table ) + .appendChild( tr ) + .appendChild( trChild ); + + trStyle = window.getComputedStyle( tr ); + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; + + documentElement.removeChild( table ); + } + return reliableTrDimensionsVal; + } + } ); +} )(); + +function curCSS( elem, name, computed ) { + var width, minWidth, maxWidth, ret, + isCustomProp = rcustomProp.test( name ), + + style = elem.style; + + computed = computed || getStyles( elem ); + + if ( computed ) { + + ret = computed.getPropertyValue( name ) || computed[ name ]; + + if ( isCustomProp && ret ) { + + ret = ret.replace( rtrimCSS, "$1" ) || undefined; + } + + if ( ret === "" && !isAttached( elem ) ) { + ret = jQuery.style( elem, name ); + } + + if ( !support.pixelBoxStyles() && rnumnonpx.test( ret ) && rboxStyle.test( name ) ) { + + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret !== undefined ? + + ret + "" : + ret; +} + +function addGetHookIf( conditionFn, hookFn ) { + + return { + get: function() { + if ( conditionFn() ) { + + delete this.get; + return; + } + + return ( this.get = hookFn ).apply( this, arguments ); + } + }; +} + +var cssPrefixes = [ "Webkit", "Moz", "ms" ], + emptyStyle = document.createElement( "div" ).style, + vendorProps = {}; + +function vendorPropName( name ) { + + var capName = name[ 0 ].toUpperCase() + name.slice( 1 ), + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in emptyStyle ) { + return name; + } + } +} + +function finalPropName( name ) { + var final = jQuery.cssProps[ name ] || vendorProps[ name ]; + + if ( final ) { + return final; + } + if ( name in emptyStyle ) { + return name; + } + return vendorProps[ name ] = vendorPropName( name ) || name; +} + +var + + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: "0", + fontWeight: "400" + }; + +function setPositiveNumber( _elem, value, subtract ) { + + var matches = rcssNum.exec( value ); + return matches ? + + Math.max( 0, matches[ 2 ] - ( subtract || 0 ) ) + ( matches[ 3 ] || "px" ) : + value; +} + +function boxModelAdjustment( elem, dimension, box, isBorderBox, styles, computedVal ) { + var i = dimension === "width" ? 1 : 0, + extra = 0, + delta = 0, + marginDelta = 0; + + if ( box === ( isBorderBox ? "border" : "content" ) ) { + return 0; + } + + for ( ; i < 4; i += 2 ) { + + if ( box === "margin" ) { + marginDelta += jQuery.css( elem, box + cssExpand[ i ], true, styles ); + } + + if ( !isBorderBox ) { + + delta += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + if ( box !== "padding" ) { + delta += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + + } else { + extra += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + + } else { + + if ( box === "content" ) { + delta -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + if ( box !== "margin" ) { + delta -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + if ( !isBorderBox && computedVal >= 0 ) { + + delta += Math.max( 0, Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + computedVal - + delta - + extra - + 0.5 + + ) ) || 0; + } + + return delta + marginDelta; +} + +function getWidthOrHeight( elem, dimension, extra ) { + + var styles = getStyles( elem ), + + boxSizingNeeded = !support.boxSizingReliable() || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + valueIsBorderBox = isBorderBox, + + val = curCSS( elem, dimension, styles ), + offsetProp = "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ); + + if ( rnumnonpx.test( val ) ) { + if ( !extra ) { + return val; + } + val = "auto"; + } + + if ( ( !support.boxSizingReliable() && isBorderBox || + + !support.reliableTrDimensions() && nodeName( elem, "tr" ) || + + val === "auto" || + + !parseFloat( val ) && jQuery.css( elem, "display", false, styles ) === "inline" ) && + + elem.getClientRects().length ) { + + isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + valueIsBorderBox = offsetProp in elem; + if ( valueIsBorderBox ) { + val = elem[ offsetProp ]; + } + } + + val = parseFloat( val ) || 0; + + return ( val + + boxModelAdjustment( + elem, + dimension, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles, + + val + ) + ) + "px"; +} + +jQuery.extend( { + + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + cssNumber: { + animationIterationCount: true, + aspectRatio: true, + borderImageSlice: true, + columnCount: true, + flexGrow: true, + flexShrink: true, + fontWeight: true, + gridArea: true, + gridColumn: true, + gridColumnEnd: true, + gridColumnStart: true, + gridRow: true, + gridRowEnd: true, + gridRowStart: true, + lineHeight: true, + opacity: true, + order: true, + orphans: true, + scale: true, + widows: true, + zIndex: true, + zoom: true, + + fillOpacity: true, + floodOpacity: true, + stopOpacity: true, + strokeMiterlimit: true, + strokeOpacity: true + }, + + cssProps: {}, + + style: function( elem, name, value, extra ) { + + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + var ret, type, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ), + style = elem.style; + + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + if ( value !== undefined ) { + type = typeof value; + + if ( type === "string" && ( ret = rcssNum.exec( value ) ) && ret[ 1 ] ) { + value = adjustCSS( elem, name, ret ); + + type = "number"; + } + + if ( value == null || value !== value ) { + return; + } + + if ( type === "number" && !isCustomProp ) { + value += ret && ret[ 3 ] || ( jQuery.cssNumber[ origName ] ? "" : "px" ); + } + + if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) { + style[ name ] = "inherit"; + } + + if ( !hooks || !( "set" in hooks ) || + ( value = hooks.set( elem, value, extra ) ) !== undefined ) { + + if ( isCustomProp ) { + style.setProperty( name, value ); + } else { + style[ name ] = value; + } + } + + } else { + + if ( hooks && "get" in hooks && + ( ret = hooks.get( elem, false, extra ) ) !== undefined ) { + + return ret; + } + + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var val, num, hooks, + origName = camelCase( name ), + isCustomProp = rcustomProp.test( name ); + + if ( !isCustomProp ) { + name = finalPropName( origName ); + } + + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || isFinite( num ) ? num || 0 : val; + } + + return val; + } +} ); + +jQuery.each( [ "height", "width" ], function( _i, dimension ) { + jQuery.cssHooks[ dimension ] = { + get: function( elem, computed, extra ) { + if ( computed ) { + + return rdisplayswap.test( jQuery.css( elem, "display" ) ) && + + ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); + } + }, + + set: function( elem, value, extra ) { + var matches, + styles = getStyles( elem ), + + scrollboxSizeBuggy = !support.scrollboxSize() && + styles.position === "absolute", + + boxSizingNeeded = scrollboxSizeBuggy || extra, + isBorderBox = boxSizingNeeded && + jQuery.css( elem, "boxSizing", false, styles ) === "border-box", + subtract = extra ? + boxModelAdjustment( + elem, + dimension, + extra, + isBorderBox, + styles + ) : + 0; + + if ( isBorderBox && scrollboxSizeBuggy ) { + subtract -= Math.ceil( + elem[ "offset" + dimension[ 0 ].toUpperCase() + dimension.slice( 1 ) ] - + parseFloat( styles[ dimension ] ) - + boxModelAdjustment( elem, dimension, "border", false, styles ) - + 0.5 + ); + } + + if ( subtract && ( matches = rcssNum.exec( value ) ) && + ( matches[ 3 ] || "px" ) !== "px" ) { + + elem.style[ dimension ] = value; + value = jQuery.css( elem, dimension ); + } + + return setPositiveNumber( elem, value, subtract ); + } + }; +} ); + +jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, + function( elem, computed ) { + if ( computed ) { + return ( parseFloat( curCSS( elem, "marginLeft" ) ) || + elem.getBoundingClientRect().left - + swap( elem, { marginLeft: 0 }, function() { + return elem.getBoundingClientRect().left; + } ) + ) + "px"; + } + } +); + +jQuery.each( { + margin: "", + padding: "", + border: "Width" +}, function( prefix, suffix ) { + jQuery.cssHooks[ prefix + suffix ] = { + expand: function( value ) { + var i = 0, + expanded = {}, + + parts = typeof value === "string" ? value.split( " " ) : [ value ]; + + for ( ; i < 4; i++ ) { + expanded[ prefix + cssExpand[ i ] + suffix ] = + parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; + } + + return expanded; + } + }; + + if ( prefix !== "margin" ) { + jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; + } +} ); + +jQuery.fn.extend( { + css: function( name, value ) { + return access( this, function( elem, name, value ) { + var styles, len, + map = {}, + i = 0; + + if ( Array.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + } +} ); + +function Tween( elem, options, prop, end, easing ) { + return new Tween.prototype.init( elem, options, prop, end, easing ); +} +jQuery.Tween = Tween; + +Tween.prototype = { + constructor: Tween, + init: function( elem, options, prop, end, easing, unit ) { + this.elem = elem; + this.prop = prop; + this.easing = easing || jQuery.easing._default; + this.options = options; + this.start = this.now = this.cur(); + this.end = end; + this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); + }, + cur: function() { + var hooks = Tween.propHooks[ this.prop ]; + + return hooks && hooks.get ? + hooks.get( this ) : + Tween.propHooks._default.get( this ); + }, + run: function( percent ) { + var eased, + hooks = Tween.propHooks[ this.prop ]; + + if ( this.options.duration ) { + this.pos = eased = jQuery.easing[ this.easing ]( + percent, this.options.duration * percent, 0, 1, this.options.duration + ); + } else { + this.pos = eased = percent; + } + this.now = ( this.end - this.start ) * eased + this.start; + + if ( this.options.step ) { + this.options.step.call( this.elem, this.now, this ); + } + + if ( hooks && hooks.set ) { + hooks.set( this ); + } else { + Tween.propHooks._default.set( this ); + } + return this; + } +}; + +Tween.prototype.init.prototype = Tween.prototype; + +Tween.propHooks = { + _default: { + get: function( tween ) { + var result; + + if ( tween.elem.nodeType !== 1 || + tween.elem[ tween.prop ] != null && tween.elem.style[ tween.prop ] == null ) { + return tween.elem[ tween.prop ]; + } + + result = jQuery.css( tween.elem, tween.prop, "" ); + + return !result || result === "auto" ? 0 : result; + }, + set: function( tween ) { + + if ( jQuery.fx.step[ tween.prop ] ) { + jQuery.fx.step[ tween.prop ]( tween ); + } else if ( tween.elem.nodeType === 1 && ( + jQuery.cssHooks[ tween.prop ] || + tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { + jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); + } else { + tween.elem[ tween.prop ] = tween.now; + } + } + } +}; + +Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { + set: function( tween ) { + if ( tween.elem.nodeType && tween.elem.parentNode ) { + tween.elem[ tween.prop ] = tween.now; + } + } +}; + +jQuery.easing = { + linear: function( p ) { + return p; + }, + swing: function( p ) { + return 0.5 - Math.cos( p * Math.PI ) / 2; + }, + _default: "swing" +}; + +jQuery.fx = Tween.prototype.init; + +jQuery.fx.step = {}; + +var + fxNow, inProgress, + rfxtypes = /^(?:toggle|show|hide)$/, + rrun = /queueHooks$/; + +function schedule() { + if ( inProgress ) { + if ( document.hidden === false && window.requestAnimationFrame ) { + window.requestAnimationFrame( schedule ); + } else { + window.setTimeout( schedule, jQuery.fx.interval ); + } + + jQuery.fx.tick(); + } +} + +function createFxNow() { + window.setTimeout( function() { + fxNow = undefined; + } ); + return ( fxNow = Date.now() ); +} + +function genFx( type, includeWidth ) { + var which, + i = 0, + attrs = { height: type }; + + includeWidth = includeWidth ? 1 : 0; + for ( ; i < 4; i += 2 - includeWidth ) { + which = cssExpand[ i ]; + attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; + } + + if ( includeWidth ) { + attrs.opacity = attrs.width = type; + } + + return attrs; +} + +function createTween( value, prop, animation ) { + var tween, + collection = ( Animation.tweeners[ prop ] || [] ).concat( Animation.tweeners[ "*" ] ), + index = 0, + length = collection.length; + for ( ; index < length; index++ ) { + if ( ( tween = collection[ index ].call( animation, prop, value ) ) ) { + + return tween; + } + } +} + +function defaultPrefilter( elem, props, opts ) { + var prop, value, toggle, hooks, oldfire, propTween, restoreDisplay, display, + isBox = "width" in props || "height" in props, + anim = this, + orig = {}, + style = elem.style, + hidden = elem.nodeType && isHiddenWithinTree( elem ), + dataShow = dataPriv.get( elem, "fxshow" ); + + if ( !opts.queue ) { + hooks = jQuery._queueHooks( elem, "fx" ); + if ( hooks.unqueued == null ) { + hooks.unqueued = 0; + oldfire = hooks.empty.fire; + hooks.empty.fire = function() { + if ( !hooks.unqueued ) { + oldfire(); + } + }; + } + hooks.unqueued++; + + anim.always( function() { + + anim.always( function() { + hooks.unqueued--; + if ( !jQuery.queue( elem, "fx" ).length ) { + hooks.empty.fire(); + } + } ); + } ); + } + + for ( prop in props ) { + value = props[ prop ]; + if ( rfxtypes.test( value ) ) { + delete props[ prop ]; + toggle = toggle || value === "toggle"; + if ( value === ( hidden ? "hide" : "show" ) ) { + + if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { + hidden = true; + + } else { + continue; + } + } + orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); + } + } + + propTween = !jQuery.isEmptyObject( props ); + if ( !propTween && jQuery.isEmptyObject( orig ) ) { + return; + } + + if ( isBox && elem.nodeType === 1 ) { + + opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; + + restoreDisplay = dataShow && dataShow.display; + if ( restoreDisplay == null ) { + restoreDisplay = dataPriv.get( elem, "display" ); + } + display = jQuery.css( elem, "display" ); + if ( display === "none" ) { + if ( restoreDisplay ) { + display = restoreDisplay; + } else { + + showHide( [ elem ], true ); + restoreDisplay = elem.style.display || restoreDisplay; + display = jQuery.css( elem, "display" ); + showHide( [ elem ] ); + } + } + + if ( display === "inline" || display === "inline-block" && restoreDisplay != null ) { + if ( jQuery.css( elem, "float" ) === "none" ) { + + if ( !propTween ) { + anim.done( function() { + style.display = restoreDisplay; + } ); + if ( restoreDisplay == null ) { + display = style.display; + restoreDisplay = display === "none" ? "" : display; + } + } + style.display = "inline-block"; + } + } + } + + if ( opts.overflow ) { + style.overflow = "hidden"; + anim.always( function() { + style.overflow = opts.overflow[ 0 ]; + style.overflowX = opts.overflow[ 1 ]; + style.overflowY = opts.overflow[ 2 ]; + } ); + } + + propTween = false; + for ( prop in orig ) { + + if ( !propTween ) { + if ( dataShow ) { + if ( "hidden" in dataShow ) { + hidden = dataShow.hidden; + } + } else { + dataShow = dataPriv.access( elem, "fxshow", { display: restoreDisplay } ); + } + + if ( toggle ) { + dataShow.hidden = !hidden; + } + + if ( hidden ) { + showHide( [ elem ], true ); + } + + anim.done( function() { + + if ( !hidden ) { + showHide( [ elem ] ); + } + dataPriv.remove( elem, "fxshow" ); + for ( prop in orig ) { + jQuery.style( elem, prop, orig[ prop ] ); + } + } ); + } + + propTween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); + if ( !( prop in dataShow ) ) { + dataShow[ prop ] = propTween.start; + if ( hidden ) { + propTween.end = propTween.start; + propTween.start = 0; + } + } + } +} + +function propFilter( props, specialEasing ) { + var index, name, easing, value, hooks; + + for ( index in props ) { + name = camelCase( index ); + easing = specialEasing[ name ]; + value = props[ index ]; + if ( Array.isArray( value ) ) { + easing = value[ 1 ]; + value = props[ index ] = value[ 0 ]; + } + + if ( index !== name ) { + props[ name ] = value; + delete props[ index ]; + } + + hooks = jQuery.cssHooks[ name ]; + if ( hooks && "expand" in hooks ) { + value = hooks.expand( value ); + delete props[ name ]; + + for ( index in value ) { + if ( !( index in props ) ) { + props[ index ] = value[ index ]; + specialEasing[ index ] = easing; + } + } + } else { + specialEasing[ name ] = easing; + } + } +} + +function Animation( elem, properties, options ) { + var result, + stopped, + index = 0, + length = Animation.prefilters.length, + deferred = jQuery.Deferred().always( function() { + + delete tick.elem; + } ), + tick = function() { + if ( stopped ) { + return false; + } + var currentTime = fxNow || createFxNow(), + remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), + + temp = remaining / animation.duration || 0, + percent = 1 - temp, + index = 0, + length = animation.tweens.length; + + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( percent ); + } + + deferred.notifyWith( elem, [ animation, percent, remaining ] ); + + if ( percent < 1 && length ) { + return remaining; + } + + if ( !length ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + } + + deferred.resolveWith( elem, [ animation ] ); + return false; + }, + animation = deferred.promise( { + elem: elem, + props: jQuery.extend( {}, properties ), + opts: jQuery.extend( true, { + specialEasing: {}, + easing: jQuery.easing._default + }, options ), + originalProperties: properties, + originalOptions: options, + startTime: fxNow || createFxNow(), + duration: options.duration, + tweens: [], + createTween: function( prop, end ) { + var tween = jQuery.Tween( elem, animation.opts, prop, end, + animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.tweens.push( tween ); + return tween; + }, + stop: function( gotoEnd ) { + var index = 0, + + length = gotoEnd ? animation.tweens.length : 0; + if ( stopped ) { + return this; + } + stopped = true; + for ( ; index < length; index++ ) { + animation.tweens[ index ].run( 1 ); + } + + if ( gotoEnd ) { + deferred.notifyWith( elem, [ animation, 1, 0 ] ); + deferred.resolveWith( elem, [ animation, gotoEnd ] ); + } else { + deferred.rejectWith( elem, [ animation, gotoEnd ] ); + } + return this; + } + } ), + props = animation.props; + + propFilter( props, animation.opts.specialEasing ); + + for ( ; index < length; index++ ) { + result = Animation.prefilters[ index ].call( animation, elem, props, animation.opts ); + if ( result ) { + if ( isFunction( result.stop ) ) { + jQuery._queueHooks( animation.elem, animation.opts.queue ).stop = + result.stop.bind( result ); + } + return result; + } + } + + jQuery.map( props, createTween, animation ); + + if ( isFunction( animation.opts.start ) ) { + animation.opts.start.call( elem, animation ); + } + + animation + .progress( animation.opts.progress ) + .done( animation.opts.done, animation.opts.complete ) + .fail( animation.opts.fail ) + .always( animation.opts.always ); + + jQuery.fx.timer( + jQuery.extend( tick, { + elem: elem, + anim: animation, + queue: animation.opts.queue + } ) + ); + + return animation; +} + +jQuery.Animation = jQuery.extend( Animation, { + + tweeners: { + "*": [ function( prop, value ) { + var tween = this.createTween( prop, value ); + adjustCSS( tween.elem, prop, rcssNum.exec( value ), tween ); + return tween; + } ] + }, + + tweener: function( props, callback ) { + if ( isFunction( props ) ) { + callback = props; + props = [ "*" ]; + } else { + props = props.match( rnothtmlwhite ); + } + + var prop, + index = 0, + length = props.length; + + for ( ; index < length; index++ ) { + prop = props[ index ]; + Animation.tweeners[ prop ] = Animation.tweeners[ prop ] || []; + Animation.tweeners[ prop ].unshift( callback ); + } + }, + + prefilters: [ defaultPrefilter ], + + prefilter: function( callback, prepend ) { + if ( prepend ) { + Animation.prefilters.unshift( callback ); + } else { + Animation.prefilters.push( callback ); + } + } +} ); + +jQuery.speed = function( speed, easing, fn ) { + var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { + complete: fn || !fn && easing || + isFunction( speed ) && speed, + duration: speed, + easing: fn && easing || easing && !isFunction( easing ) && easing + }; + + if ( jQuery.fx.off ) { + opt.duration = 0; + + } else { + if ( typeof opt.duration !== "number" ) { + if ( opt.duration in jQuery.fx.speeds ) { + opt.duration = jQuery.fx.speeds[ opt.duration ]; + + } else { + opt.duration = jQuery.fx.speeds._default; + } + } + } + + if ( opt.queue == null || opt.queue === true ) { + opt.queue = "fx"; + } + + opt.old = opt.complete; + + opt.complete = function() { + if ( isFunction( opt.old ) ) { + opt.old.call( this ); + } + + if ( opt.queue ) { + jQuery.dequeue( this, opt.queue ); + } + }; + + return opt; +}; + +jQuery.fn.extend( { + fadeTo: function( speed, to, easing, callback ) { + + return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show() + + .end().animate( { opacity: to }, speed, easing, callback ); + }, + animate: function( prop, speed, easing, callback ) { + var empty = jQuery.isEmptyObject( prop ), + optall = jQuery.speed( speed, easing, callback ), + doAnimation = function() { + + var anim = Animation( this, jQuery.extend( {}, prop ), optall ); + + if ( empty || dataPriv.get( this, "finish" ) ) { + anim.stop( true ); + } + }; + + doAnimation.finish = doAnimation; + + return empty || optall.queue === false ? + this.each( doAnimation ) : + this.queue( optall.queue, doAnimation ); + }, + stop: function( type, clearQueue, gotoEnd ) { + var stopQueue = function( hooks ) { + var stop = hooks.stop; + delete hooks.stop; + stop( gotoEnd ); + }; + + if ( typeof type !== "string" ) { + gotoEnd = clearQueue; + clearQueue = type; + type = undefined; + } + if ( clearQueue ) { + this.queue( type || "fx", [] ); + } + + return this.each( function() { + var dequeue = true, + index = type != null && type + "queueHooks", + timers = jQuery.timers, + data = dataPriv.get( this ); + + if ( index ) { + if ( data[ index ] && data[ index ].stop ) { + stopQueue( data[ index ] ); + } + } else { + for ( index in data ) { + if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { + stopQueue( data[ index ] ); + } + } + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && + ( type == null || timers[ index ].queue === type ) ) { + + timers[ index ].anim.stop( gotoEnd ); + dequeue = false; + timers.splice( index, 1 ); + } + } + + if ( dequeue || !gotoEnd ) { + jQuery.dequeue( this, type ); + } + } ); + }, + finish: function( type ) { + if ( type !== false ) { + type = type || "fx"; + } + return this.each( function() { + var index, + data = dataPriv.get( this ), + queue = data[ type + "queue" ], + hooks = data[ type + "queueHooks" ], + timers = jQuery.timers, + length = queue ? queue.length : 0; + + data.finish = true; + + jQuery.queue( this, type, [] ); + + if ( hooks && hooks.stop ) { + hooks.stop.call( this, true ); + } + + for ( index = timers.length; index--; ) { + if ( timers[ index ].elem === this && timers[ index ].queue === type ) { + timers[ index ].anim.stop( true ); + timers.splice( index, 1 ); + } + } + + for ( index = 0; index < length; index++ ) { + if ( queue[ index ] && queue[ index ].finish ) { + queue[ index ].finish.call( this ); + } + } + + delete data.finish; + } ); + } +} ); + +jQuery.each( [ "toggle", "show", "hide" ], function( _i, name ) { + var cssFn = jQuery.fn[ name ]; + jQuery.fn[ name ] = function( speed, easing, callback ) { + return speed == null || typeof speed === "boolean" ? + cssFn.apply( this, arguments ) : + this.animate( genFx( name, true ), speed, easing, callback ); + }; +} ); + +jQuery.each( { + slideDown: genFx( "show" ), + slideUp: genFx( "hide" ), + slideToggle: genFx( "toggle" ), + fadeIn: { opacity: "show" }, + fadeOut: { opacity: "hide" }, + fadeToggle: { opacity: "toggle" } +}, function( name, props ) { + jQuery.fn[ name ] = function( speed, easing, callback ) { + return this.animate( props, speed, easing, callback ); + }; +} ); + +jQuery.timers = []; +jQuery.fx.tick = function() { + var timer, + i = 0, + timers = jQuery.timers; + + fxNow = Date.now(); + + for ( ; i < timers.length; i++ ) { + timer = timers[ i ]; + + if ( !timer() && timers[ i ] === timer ) { + timers.splice( i--, 1 ); + } + } + + if ( !timers.length ) { + jQuery.fx.stop(); + } + fxNow = undefined; +}; + +jQuery.fx.timer = function( timer ) { + jQuery.timers.push( timer ); + jQuery.fx.start(); +}; + +jQuery.fx.interval = 13; +jQuery.fx.start = function() { + if ( inProgress ) { + return; + } + + inProgress = true; + schedule(); +}; + +jQuery.fx.stop = function() { + inProgress = null; +}; + +jQuery.fx.speeds = { + slow: 600, + fast: 200, + + _default: 400 +}; + +jQuery.fn.delay = function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = window.setTimeout( next, time ); + hooks.stop = function() { + window.clearTimeout( timeout ); + }; + } ); +}; + +( function() { + var input = document.createElement( "input" ), + select = document.createElement( "select" ), + opt = select.appendChild( document.createElement( "option" ) ); + + input.type = "checkbox"; + + support.checkOn = input.value !== ""; + + support.optSelected = opt.selected; + + input = document.createElement( "input" ); + input.value = "t"; + input.type = "radio"; + support.radioValue = input.value === "t"; +} )(); + +var boolHook, + attrHandle = jQuery.expr.attrHandle; + +jQuery.fn.extend( { + attr: function( name, value ) { + return access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each( function() { + jQuery.removeAttr( this, name ); + } ); + } +} ); + +jQuery.extend( { + attr: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + hooks = jQuery.attrHooks[ name.toLowerCase() ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : undefined ); + } + + if ( value !== undefined ) { + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + } + + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + elem.setAttribute( name, value + "" ); + return value; + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + ret = jQuery.find.attr( elem, name ); + + return ret == null ? undefined : ret; + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !support.radioValue && value === "radio" && + nodeName( elem, "input" ) ) { + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + removeAttr: function( elem, value ) { + var name, + i = 0, + + attrNames = value && value.match( rnothtmlwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( ( name = attrNames[ i++ ] ) ) { + elem.removeAttribute( name ); + } + } + } +} ); + +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + + jQuery.removeAttr( elem, name ); + } else { + elem.setAttribute( name, name ); + } + return name; + } +}; + +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( _i, name ) { + var getter = attrHandle[ name ] || jQuery.find.attr; + + attrHandle[ name ] = function( elem, name, isXML ) { + var ret, handle, + lowercaseName = name.toLowerCase(); + + if ( !isXML ) { + + handle = attrHandle[ lowercaseName ]; + attrHandle[ lowercaseName ] = ret; + ret = getter( elem, name, isXML ) != null ? + lowercaseName : + null; + attrHandle[ lowercaseName ] = handle; + } + return ret; + }; +} ); + +var rfocusable = /^(?:input|select|textarea|button)$/i, + rclickable = /^(?:a|area)$/i; + +jQuery.fn.extend( { + prop: function( name, value ) { + return access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + return this.each( function() { + delete this[ jQuery.propFix[ name ] || name ]; + } ); + } +} ); + +jQuery.extend( { + prop: function( elem, name, value ) { + var ret, hooks, + nType = elem.nodeType; + + if ( nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && + ( ret = hooks.set( elem, value, name ) ) !== undefined ) { + return ret; + } + + return ( elem[ name ] = value ); + } + + if ( hooks && "get" in hooks && ( ret = hooks.get( elem, name ) ) !== null ) { + return ret; + } + + return elem[ name ]; + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + if ( tabindex ) { + return parseInt( tabindex, 10 ); + } + + if ( + rfocusable.test( elem.nodeName ) || + rclickable.test( elem.nodeName ) && + elem.href + ) { + return 0; + } + + return -1; + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + } +} ); + +if ( !support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + + var parent = elem.parentNode; + if ( parent && parent.parentNode ) { + parent.parentNode.selectedIndex; + } + return null; + }, + set: function( elem ) { + + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + }; +} + +jQuery.each( [ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +} ); + + function stripAndCollapse( value ) { + var tokens = value.match( rnothtmlwhite ) || []; + return tokens.join( " " ); + } + +function getClass( elem ) { + return elem.getAttribute && elem.getAttribute( "class" ) || ""; +} + +function classesToArray( value ) { + if ( Array.isArray( value ) ) { + return value; + } + if ( typeof value === "string" ) { + return value.match( rnothtmlwhite ) || []; + } + return []; +} + +jQuery.fn.extend( { + addClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).addClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + if ( cur.indexOf( " " + className + " " ) < 0 ) { + cur += className + " "; + } + } + + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + removeClass: function( value ) { + var classNames, cur, curValue, className, i, finalValue; + + if ( isFunction( value ) ) { + return this.each( function( j ) { + jQuery( this ).removeClass( value.call( this, j, getClass( this ) ) ); + } ); + } + + if ( !arguments.length ) { + return this.attr( "class", "" ); + } + + classNames = classesToArray( value ); + + if ( classNames.length ) { + return this.each( function() { + curValue = getClass( this ); + + cur = this.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " ); + + if ( cur ) { + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + while ( cur.indexOf( " " + className + " " ) > -1 ) { + cur = cur.replace( " " + className + " ", " " ); + } + } + + finalValue = stripAndCollapse( cur ); + if ( curValue !== finalValue ) { + this.setAttribute( "class", finalValue ); + } + } + } ); + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var classNames, className, i, self, + type = typeof value, + isValidValue = type === "string" || Array.isArray( value ); + + if ( isFunction( value ) ) { + return this.each( function( i ) { + jQuery( this ).toggleClass( + value.call( this, i, getClass( this ), stateVal ), + stateVal + ); + } ); + } + + if ( typeof stateVal === "boolean" && isValidValue ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + classNames = classesToArray( value ); + + return this.each( function() { + if ( isValidValue ) { + + self = jQuery( this ); + + for ( i = 0; i < classNames.length; i++ ) { + className = classNames[ i ]; + + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + } else if ( value === undefined || type === "boolean" ) { + className = getClass( this ); + if ( className ) { + + dataPriv.set( this, "__className__", className ); + } + + if ( this.setAttribute ) { + this.setAttribute( "class", + className || value === false ? + "" : + dataPriv.get( this, "__className__" ) || "" + ); + } + } + } ); + }, + + hasClass: function( selector ) { + var className, elem, + i = 0; + + className = " " + selector + " "; + while ( ( elem = this[ i++ ] ) ) { + if ( elem.nodeType === 1 && + ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { + return true; + } + } + + return false; + } +} ); + +var rreturn = /\r/g; + +jQuery.fn.extend( { + val: function( value ) { + var hooks, ret, valueIsFunction, + elem = this[ 0 ]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || + jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && + "get" in hooks && + ( ret = hooks.get( elem, "value" ) ) !== undefined + ) { + return ret; + } + + ret = elem.value; + + if ( typeof ret === "string" ) { + return ret.replace( rreturn, "" ); + } + + return ret == null ? "" : ret; + } + + return; + } + + valueIsFunction = isFunction( value ); + + return this.each( function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( valueIsFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + if ( val == null ) { + val = ""; + + } else if ( typeof val === "number" ) { + val += ""; + + } else if ( Array.isArray( val ) ) { + val = jQuery.map( val, function( value ) { + return value == null ? "" : value + ""; + } ); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + if ( !hooks || !( "set" in hooks ) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + } ); + } +} ); + +jQuery.extend( { + valHooks: { + option: { + get: function( elem ) { + + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + + stripAndCollapse( jQuery.text( elem ) ); + } + }, + select: { + get: function( elem ) { + var value, option, i, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one", + values = one ? null : [], + max = one ? index + 1 : options.length; + + if ( index < 0 ) { + i = max; + + } else { + i = one ? index : 0; + } + + for ( ; i < max; i++ ) { + option = options[ i ]; + + if ( ( option.selected || i === index ) && + + !option.disabled && + ( !option.parentNode.disabled || + !nodeName( option.parentNode, "optgroup" ) ) ) { + + value = jQuery( option ).val(); + + if ( one ) { + return value; + } + + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + + if ( option.selected = + jQuery.inArray( jQuery.valHooks.option.get( option ), values ) > -1 + ) { + optionSet = true; + } + + } + + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + } +} ); + +jQuery.each( [ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( Array.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery( elem ).val(), value ) > -1 ); + } + } + }; + if ( !support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + return elem.getAttribute( "value" ) === null ? "on" : elem.value; + }; + } +} ); + +var location = window.location; + +var nonce = { guid: Date.now() }; + +var rquery = ( /\?/ ); + +jQuery.parseXML = function( data ) { + var xml, parserErrorElem; + if ( !data || typeof data !== "string" ) { + return null; + } + + try { + xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); + } catch ( e ) {} + + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); + } + return xml; +}; + +var rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + stopPropagationCallback = function( e ) { + e.stopPropagation(); + }; + +jQuery.extend( jQuery.event, { + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, lastElement, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; + + cur = lastElement = tmp = elem = elem || document; + + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "." ) > -1 ) { + + namespaces = type.split( "." ); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf( ":" ) < 0 && "on" + type; + + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join( "." ); + event.rnamespace = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : + null; + + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + if ( !onlyHandlers && !special.noBubble && !isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + if ( tmp === ( elem.ownerDocument || document ) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + i = 0; + while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { + lastElement = cur; + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && + dataPriv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( ( !special._default || + special._default.apply( eventPath.pop(), data ) === false ) && + acceptData( elem ) ) { + + if ( ontype && isFunction( elem[ type ] ) && !isWindow( elem ) ) { + + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + jQuery.event.triggered = type; + + if ( event.isPropagationStopped() ) { + lastElement.addEventListener( type, stopPropagationCallback ); + } + + elem[ type ](); + + if ( event.isPropagationStopped() ) { + lastElement.removeEventListener( type, stopPropagationCallback ); + } + + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + simulate: function( type, elem, event ) { + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true + } + ); + + jQuery.event.trigger( e, null, elem ); + } + +} ); + +jQuery.fn.extend( { + + trigger: function( type, data ) { + return this.each( function() { + jQuery.event.trigger( type, data, this ); + } ); + }, + triggerHandler: function( type, data ) { + var elem = this[ 0 ]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +} ); + +var + rbracket = /\[\]$/, + rCRLF = /\r?\n/g, + rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, + rsubmittable = /^(?:input|select|textarea|keygen)/i; + +function buildParams( prefix, obj, traditional, add ) { + var name; + + if ( Array.isArray( obj ) ) { + + jQuery.each( obj, function( i, v ) { + if ( traditional || rbracket.test( prefix ) ) { + + add( prefix, v ); + + } else { + + buildParams( + prefix + "[" + ( typeof v === "object" && v != null ? i : "" ) + "]", + v, + traditional, + add + ); + } + } ); + + } else if ( !traditional && toType( obj ) === "object" ) { + + for ( name in obj ) { + buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); + } + + } else { + + add( prefix, obj ); + } +} + +jQuery.param = function( a, traditional ) { + var prefix, + s = [], + add = function( key, valueOrFunction ) { + + var value = isFunction( valueOrFunction ) ? + valueOrFunction() : + valueOrFunction; + + s[ s.length ] = encodeURIComponent( key ) + "=" + + encodeURIComponent( value == null ? "" : value ); + }; + + if ( a == null ) { + return ""; + } + + if ( Array.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { + + jQuery.each( a, function() { + add( this.name, this.value ); + } ); + + } else { + + for ( prefix in a ) { + buildParams( prefix, a[ prefix ], traditional, add ); + } + } + + return s.join( "&" ); +}; + +jQuery.fn.extend( { + serialize: function() { + return jQuery.param( this.serializeArray() ); + }, + serializeArray: function() { + return this.map( function() { + + var elements = jQuery.prop( this, "elements" ); + return elements ? jQuery.makeArray( elements ) : this; + } ).filter( function() { + var type = this.type; + + return this.name && !jQuery( this ).is( ":disabled" ) && + rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && + ( this.checked || !rcheckableType.test( type ) ); + } ).map( function( _i, elem ) { + var val = jQuery( this ).val(); + + if ( val == null ) { + return null; + } + + if ( Array.isArray( val ) ) { + return jQuery.map( val, function( val ) { + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ); + } + + return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; + } ).get(); + } +} ); + +var + r20 = /%20/g, + rhash = /#.*$/, + rantiCache = /([?&])_=[^&]*/, + rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg, + + rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, + rnoContent = /^(?:GET|HEAD)$/, + rprotocol = /^\/\ + + prefilters = {}, + + transports = {}, + + allTypes = "*/".concat( "*" ), + + originAnchor = document.createElement( "a" ); + +originAnchor.href = location.href; + +function addToPrefiltersOrTransports( structure ) { + + return function( dataTypeExpression, func ) { + + if ( typeof dataTypeExpression !== "string" ) { + func = dataTypeExpression; + dataTypeExpression = "*"; + } + + var dataType, + i = 0, + dataTypes = dataTypeExpression.toLowerCase().match( rnothtmlwhite ) || []; + + if ( isFunction( func ) ) { + + while ( ( dataType = dataTypes[ i++ ] ) ) { + + if ( dataType[ 0 ] === "+" ) { + dataType = dataType.slice( 1 ) || "*"; + ( structure[ dataType ] = structure[ dataType ] || [] ).unshift( func ); + + } else { + ( structure[ dataType ] = structure[ dataType ] || [] ).push( func ); + } + } + } + }; +} + +function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { + + var inspected = {}, + seekingTransport = ( structure === transports ); + + function inspect( dataType ) { + var selected; + inspected[ dataType ] = true; + jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { + var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); + if ( typeof dataTypeOrTransport === "string" && + !seekingTransport && !inspected[ dataTypeOrTransport ] ) { + + options.dataTypes.unshift( dataTypeOrTransport ); + inspect( dataTypeOrTransport ); + return false; + } else if ( seekingTransport ) { + return !( selected = dataTypeOrTransport ); + } + } ); + return selected; + } + + return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); +} + +function ajaxExtend( target, src ) { + var key, deep, + flatOptions = jQuery.ajaxSettings.flatOptions || {}; + + for ( key in src ) { + if ( src[ key ] !== undefined ) { + ( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ]; + } + } + if ( deep ) { + jQuery.extend( true, target, deep ); + } + + return target; +} + +function ajaxHandleResponses( s, jqXHR, responses ) { + + var ct, type, finalDataType, firstDataType, + contents = s.contents, + dataTypes = s.dataTypes; + + while ( dataTypes[ 0 ] === "*" ) { + dataTypes.shift(); + if ( ct === undefined ) { + ct = s.mimeType || jqXHR.getResponseHeader( "Content-Type" ); + } + } + + if ( ct ) { + for ( type in contents ) { + if ( contents[ type ] && contents[ type ].test( ct ) ) { + dataTypes.unshift( type ); + break; + } + } + } + + if ( dataTypes[ 0 ] in responses ) { + finalDataType = dataTypes[ 0 ]; + } else { + + for ( type in responses ) { + if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[ 0 ] ] ) { + finalDataType = type; + break; + } + if ( !firstDataType ) { + firstDataType = type; + } + } + + finalDataType = finalDataType || firstDataType; + } + + if ( finalDataType ) { + if ( finalDataType !== dataTypes[ 0 ] ) { + dataTypes.unshift( finalDataType ); + } + return responses[ finalDataType ]; + } +} + +function ajaxConvert( s, response, jqXHR, isSuccess ) { + var conv2, current, conv, tmp, prev, + converters = {}, + + dataTypes = s.dataTypes.slice(); + + if ( dataTypes[ 1 ] ) { + for ( conv in s.converters ) { + converters[ conv.toLowerCase() ] = s.converters[ conv ]; + } + } + + current = dataTypes.shift(); + + while ( current ) { + + if ( s.responseFields[ current ] ) { + jqXHR[ s.responseFields[ current ] ] = response; + } + + if ( !prev && isSuccess && s.dataFilter ) { + response = s.dataFilter( response, s.dataType ); + } + + prev = current; + current = dataTypes.shift(); + + if ( current ) { + + if ( current === "*" ) { + + current = prev; + + } else if ( prev !== "*" && prev !== current ) { + + conv = converters[ prev + " " + current ] || converters[ "* " + current ]; + + if ( !conv ) { + for ( conv2 in converters ) { + + tmp = conv2.split( " " ); + if ( tmp[ 1 ] === current ) { + + conv = converters[ prev + " " + tmp[ 0 ] ] || + converters[ "* " + tmp[ 0 ] ]; + if ( conv ) { + + if ( conv === true ) { + conv = converters[ conv2 ]; + + } else if ( converters[ conv2 ] !== true ) { + current = tmp[ 0 ]; + dataTypes.unshift( tmp[ 1 ] ); + } + break; + } + } + } + } + + if ( conv !== true ) { + + if ( conv && s.throws ) { + response = conv( response ); + } else { + try { + response = conv( response ); + } catch ( e ) { + return { + state: "parsererror", + error: conv ? e : "No conversion from " + prev + " to " + current + }; + } + } + } + } + } + } + + return { state: "success", data: response }; +} + +jQuery.extend( { + + active: 0, + + lastModified: {}, + etag: {}, + + ajaxSettings: { + url: location.href, + type: "GET", + isLocal: rlocalProtocol.test( location.protocol ), + global: true, + processData: true, + async: true, + contentType: "application/x-www-form-urlencoded; charset=UTF-8", + + accepts: { + "*": allTypes, + text: "text/plain", + html: "text/html", + xml: "application/xml, text/xml", + json: "application/json, text/javascript" + }, + + contents: { + xml: /\bxml\b/, + html: /\bhtml/, + json: /\bjson\b/ + }, + + responseFields: { + xml: "responseXML", + text: "responseText", + json: "responseJSON" + }, + + converters: { + + "* text": String, + + "text html": true, + + "text json": JSON.parse, + + "text xml": jQuery.parseXML + }, + + flatOptions: { + url: true, + context: true + } + }, + + ajaxSetup: function( target, settings ) { + return settings ? + + ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : + + ajaxExtend( jQuery.ajaxSettings, target ); + }, + + ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), + ajaxTransport: addToPrefiltersOrTransports( transports ), + + ajax: function( url, options ) { + + if ( typeof url === "object" ) { + options = url; + url = undefined; + } + + options = options || {}; + + var transport, + + cacheURL, + + responseHeadersString, + responseHeaders, + + timeoutTimer, + + urlAnchor, + + completed, + + fireGlobals, + + i, + + uncached, + + s = jQuery.ajaxSetup( {}, options ), + + callbackContext = s.context || s, + + globalEventContext = s.context && + ( callbackContext.nodeType || callbackContext.jquery ) ? + jQuery( callbackContext ) : + jQuery.event, + + deferred = jQuery.Deferred(), + completeDeferred = jQuery.Callbacks( "once memory" ), + + statusCode = s.statusCode || {}, + + requestHeaders = {}, + requestHeadersNames = {}, + + strAbort = "canceled", + + jqXHR = { + readyState: 0, + + getResponseHeader: function( key ) { + var match; + if ( completed ) { + if ( !responseHeaders ) { + responseHeaders = {}; + while ( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() + " " ] = + ( responseHeaders[ match[ 1 ].toLowerCase() + " " ] || [] ) + .concat( match[ 2 ] ); + } + } + match = responseHeaders[ key.toLowerCase() + " " ]; + } + return match == null ? null : match.join( ", " ); + }, + + getAllResponseHeaders: function() { + return completed ? responseHeadersString : null; + }, + + setRequestHeader: function( name, value ) { + if ( completed == null ) { + name = requestHeadersNames[ name.toLowerCase() ] = + requestHeadersNames[ name.toLowerCase() ] || name; + requestHeaders[ name ] = value; + } + return this; + }, + + overrideMimeType: function( type ) { + if ( completed == null ) { + s.mimeType = type; + } + return this; + }, + + statusCode: function( map ) { + var code; + if ( map ) { + if ( completed ) { + + jqXHR.always( map[ jqXHR.status ] ); + } else { + + for ( code in map ) { + statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; + } + } + } + return this; + }, + + abort: function( statusText ) { + var finalText = statusText || strAbort; + if ( transport ) { + transport.abort( finalText ); + } + done( 0, finalText ); + return this; + } + }; + + deferred.promise( jqXHR ); + + s.url = ( ( url || s.url || location.href ) + "" ) + .replace( rprotocol, location.protocol + "//" ); + + s.type = options.method || options.type || s.method || s.type; + + s.dataTypes = ( s.dataType || "*" ).toLowerCase().match( rnothtmlwhite ) || [ "" ]; + + if ( s.crossDomain == null ) { + urlAnchor = document.createElement( "a" ); + + try { + urlAnchor.href = s.url; + + urlAnchor.href = urlAnchor.href; + s.crossDomain = originAnchor.protocol + "//" + originAnchor.host !== + urlAnchor.protocol + "//" + urlAnchor.host; + } catch ( e ) { + + s.crossDomain = true; + } + } + + if ( s.data && s.processData && typeof s.data !== "string" ) { + s.data = jQuery.param( s.data, s.traditional ); + } + + inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); + + if ( completed ) { + return jqXHR; + } + + fireGlobals = jQuery.event && s.global; + + if ( fireGlobals && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + s.type = s.type.toUpperCase(); + + s.hasContent = !rnoContent.test( s.type ); + + cacheURL = s.url.replace( rhash, "" ); + + if ( !s.hasContent ) { + + uncached = s.url.slice( cacheURL.length ); + + if ( s.data && ( s.processData || typeof s.data === "string" ) ) { + cacheURL += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data; + + delete s.data; + } + + if ( s.cache === false ) { + cacheURL = cacheURL.replace( rantiCache, "$1" ); + uncached = ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ( nonce.guid++ ) + + uncached; + } + + s.url = cacheURL + uncached; + + } else if ( s.data && s.processData && + ( s.contentType || "" ).indexOf( "application/x-www-form-urlencoded" ) === 0 ) { + s.data = s.data.replace( r20, "+" ); + } + + if ( s.ifModified ) { + if ( jQuery.lastModified[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); + } + if ( jQuery.etag[ cacheURL ] ) { + jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); + } + } + + if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { + jqXHR.setRequestHeader( "Content-Type", s.contentType ); + } + + jqXHR.setRequestHeader( + "Accept", + s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[ 0 ] ] ? + s.accepts[ s.dataTypes[ 0 ] ] + + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : + s.accepts[ "*" ] + ); + + for ( i in s.headers ) { + jqXHR.setRequestHeader( i, s.headers[ i ] ); + } + + if ( s.beforeSend && + ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || completed ) ) { + + return jqXHR.abort(); + } + + strAbort = "abort"; + + completeDeferred.add( s.complete ); + jqXHR.done( s.success ); + jqXHR.fail( s.error ); + + transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); + + if ( !transport ) { + done( -1, "No Transport" ); + } else { + jqXHR.readyState = 1; + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); + } + + if ( completed ) { + return jqXHR; + } + + if ( s.async && s.timeout > 0 ) { + timeoutTimer = window.setTimeout( function() { + jqXHR.abort( "timeout" ); + }, s.timeout ); + } + + try { + completed = false; + transport.send( requestHeaders, done ); + } catch ( e ) { + + if ( completed ) { + throw e; + } + + done( -1, e ); + } + } + + function done( status, nativeStatusText, responses, headers ) { + var isSuccess, success, error, response, modified, + statusText = nativeStatusText; + + if ( completed ) { + return; + } + + completed = true; + + if ( timeoutTimer ) { + window.clearTimeout( timeoutTimer ); + } + + transport = undefined; + + responseHeadersString = headers || ""; + + jqXHR.readyState = status > 0 ? 4 : 0; + + isSuccess = status >= 200 && status < 300 || status === 304; + + if ( responses ) { + response = ajaxHandleResponses( s, jqXHR, responses ); + } + + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { + s.converters[ "text script" ] = function() {}; + } + + response = ajaxConvert( s, response, jqXHR, isSuccess ); + + if ( isSuccess ) { + + if ( s.ifModified ) { + modified = jqXHR.getResponseHeader( "Last-Modified" ); + if ( modified ) { + jQuery.lastModified[ cacheURL ] = modified; + } + modified = jqXHR.getResponseHeader( "etag" ); + if ( modified ) { + jQuery.etag[ cacheURL ] = modified; + } + } + + if ( status === 204 || s.type === "HEAD" ) { + statusText = "nocontent"; + + } else if ( status === 304 ) { + statusText = "notmodified"; + + } else { + statusText = response.state; + success = response.data; + error = response.error; + isSuccess = !error; + } + } else { + + error = statusText; + if ( status || !statusText ) { + statusText = "error"; + if ( status < 0 ) { + status = 0; + } + } + } + + jqXHR.status = status; + jqXHR.statusText = ( nativeStatusText || statusText ) + ""; + + if ( isSuccess ) { + deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); + } else { + deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); + } + + jqXHR.statusCode( statusCode ); + statusCode = undefined; + + if ( fireGlobals ) { + globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", + [ jqXHR, s, isSuccess ? success : error ] ); + } + + completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); + + if ( fireGlobals ) { + globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); + + if ( !( --jQuery.active ) ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + return jqXHR; + }, + + getJSON: function( url, data, callback ) { + return jQuery.get( url, data, callback, "json" ); + }, + + getScript: function( url, callback ) { + return jQuery.get( url, undefined, callback, "script" ); + } +} ); + +jQuery.each( [ "get", "post" ], function( _i, method ) { + jQuery[ method ] = function( url, data, callback, type ) { + + if ( isFunction( data ) ) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax( jQuery.extend( { + url: url, + type: method, + dataType: type, + data: data, + success: callback + }, jQuery.isPlainObject( url ) && url ) ); + }; +} ); + +jQuery.ajaxPrefilter( function( s ) { + var i; + for ( i in s.headers ) { + if ( i.toLowerCase() === "content-type" ) { + s.contentType = s.headers[ i ] || ""; + } + } +} ); + +jQuery._evalUrl = function( url, options, doc ) { + return jQuery.ajax( { + url: url, + + type: "GET", + dataType: "script", + cache: true, + async: false, + global: false, + + converters: { + "text script": function() {} + }, + dataFilter: function( response ) { + jQuery.globalEval( response, options, doc ); + } + } ); +}; + +jQuery.fn.extend( { + wrapAll: function( html ) { + var wrap; + + if ( this[ 0 ] ) { + if ( isFunction( html ) ) { + html = html.call( this[ 0 ] ); + } + + wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true ); + + if ( this[ 0 ].parentNode ) { + wrap.insertBefore( this[ 0 ] ); + } + + wrap.map( function() { + var elem = this; + + while ( elem.firstElementChild ) { + elem = elem.firstElementChild; + } + + return elem; + } ).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( isFunction( html ) ) { + return this.each( function( i ) { + jQuery( this ).wrapInner( html.call( this, i ) ); + } ); + } + + return this.each( function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + } ); + }, + + wrap: function( html ) { + var htmlIsFunction = isFunction( html ); + + return this.each( function( i ) { + jQuery( this ).wrapAll( htmlIsFunction ? html.call( this, i ) : html ); + } ); + }, + + unwrap: function( selector ) { + this.parent( selector ).not( "body" ).each( function() { + jQuery( this ).replaceWith( this.childNodes ); + } ); + return this; + } +} ); + +jQuery.expr.pseudos.hidden = function( elem ) { + return !jQuery.expr.pseudos.visible( elem ); +}; +jQuery.expr.pseudos.visible = function( elem ) { + return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length ); +}; + +jQuery.ajaxSettings.xhr = function() { + try { + return new window.XMLHttpRequest(); + } catch ( e ) {} +}; + +var xhrSuccessStatus = { + + 0: 200, + + 1223: 204 + }, + xhrSupported = jQuery.ajaxSettings.xhr(); + +support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); +support.ajax = xhrSupported = !!xhrSupported; + +jQuery.ajaxTransport( function( options ) { + var callback, errorCallback; + + if ( support.cors || xhrSupported && !options.crossDomain ) { + return { + send: function( headers, complete ) { + var i, + xhr = options.xhr(); + + xhr.open( + options.type, + options.url, + options.async, + options.username, + options.password + ); + + if ( options.xhrFields ) { + for ( i in options.xhrFields ) { + xhr[ i ] = options.xhrFields[ i ]; + } + } + + if ( options.mimeType && xhr.overrideMimeType ) { + xhr.overrideMimeType( options.mimeType ); + } + + if ( !options.crossDomain && !headers[ "X-Requested-With" ] ) { + headers[ "X-Requested-With" ] = "XMLHttpRequest"; + } + + for ( i in headers ) { + xhr.setRequestHeader( i, headers[ i ] ); + } + +callback = function( type ) { + return function() { + if ( callback ) { + callback = errorCallback = xhr.onload = + xhr.onerror = xhr.onabort = xhr.ontimeout = + xhr.onreadystatechange = null; + + if ( type === "abort" ) { + xhr.abort(); + } else if ( type === "error" ) { + + if ( typeof xhr.status !== "number" ) { + complete( 0, "error" ); + } else { + complete( + + xhr.status, + xhr.statusText + ); + } + } else { + complete( + xhrSuccessStatus[ xhr.status ] || xhr.status, + xhr.statusText, + + ( xhr.responseType || "text" ) !== "text" || + typeof xhr.responseText !== "string" ? + { binary: xhr.response } : + { text: xhr.responseText }, + xhr.getAllResponseHeaders() + ); + } + } + }; +}; + +xhr.onload = callback(); +errorCallback = xhr.onerror = xhr.ontimeout = callback( "error" ); + +if ( xhr.onabort !== undefined ) { + xhr.onabort = errorCallback; +} else { + xhr.onreadystatechange = function() { + + if ( xhr.readyState === 4 ) { + + window.setTimeout( function() { + if ( callback ) { + errorCallback(); + } + } ); + } + }; +} + +callback = callback( "abort" ); + +try { + + xhr.send( options.hasContent && options.data || null ); +} catch ( e ) { + + if ( callback ) { + throw e; + } +} +}, + +abort: function() { +if ( callback ) { + callback(); +} +} +}; +} +} ); + +jQuery.ajaxPrefilter( function( s ) { +if ( s.crossDomain ) { +s.contents.script = false; +} +} ); + +jQuery.ajaxSetup( { +accepts: { +script: "text/javascript, application/javascript, " + +"application/ecmascript, application/x-ecmascript" +}, +contents: { +script: /\b(?:java|ecma)script\b/ +}, +converters: { +"text script": function( text ) { +jQuery.globalEval( text ); +return text; +} +} +} ); + +jQuery.ajaxPrefilter( "script", function( s ) { +if ( s.cache === undefined ) { +s.cache = false; +} +if ( s.crossDomain ) { +s.type = "GET"; +} +} ); + +jQuery.ajaxTransport( "script", function( s ) { + +if ( s.crossDomain || s.scriptAttrs ) { +var script, callback; +return { +send: function( _, complete ) { +script = jQuery( " + + + diff --git a/firmware_update/latest/data/www/edit.html b/firmware_update/latest/data/www/edit.html new file mode 100644 index 0000000..9f29e48 --- /dev/null +++ b/firmware_update/latest/data/www/edit.html @@ -0,0 +1,191 @@ + + + + Edit file + + + + + + + +

Edit file

+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/edit_old.html b/firmware_update/latest/data/www/edit_old.html new file mode 100644 index 0000000..e76f413 --- /dev/null +++ b/firmware_update/latest/data/www/edit_old.html @@ -0,0 +1,151 @@ + + + + Edit file + + + + + + +

Edit file

+
+ Editing file: {{SAVE_PATH_INPUT}} +
+
+
+ +
+
+ {{SAVE_PATH_INPUT}} + + + + +
+
+
+ + + + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/failed.html b/firmware_update/latest/data/www/failed.html new file mode 100644 index 0000000..cfcf05e --- /dev/null +++ b/firmware_update/latest/data/www/failed.html @@ -0,0 +1,33 @@ + + + + Update Failed + + + + + + + +
+

The update has failed.

+
+ +
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/files.html b/firmware_update/latest/data/www/files.html new file mode 100644 index 0000000..54f37df --- /dev/null +++ b/firmware_update/latest/data/www/files.html @@ -0,0 +1,276 @@ + + + + File Manager + + + + + + + + + +
+

File Manager

+ +
+ File list +
+

  Total: {{FS_TOTAL_BYTES}}, Used: {{FS_USED_BYTES}}, Available: {{FS_FREE_BYTES}}

+
+ + {{LISTED_FILES}} +
Listing: Size:
+
+
+ +
+ +
+ File upload +
+
+ + + + + + + + + + + +
+
+    + +
+
+
+ +
+
+
+ +
+ +
+ Edit file +
+
+ + + +
+
+
+
+ +
+ +
+ Delete file +
+
+ + + +
+
+
+
+ +
+ + + + +
+ + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/home.html b/firmware_update/latest/data/www/home.html new file mode 100644 index 0000000..84f6450 --- /dev/null +++ b/firmware_update/latest/data/www/home.html @@ -0,0 +1,303 @@ + + + + System Summary + + + + + + + +

System Summary

+
+ +
+ Booth Overview +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ System Information +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+
+ + +
+ Network / WiFi +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ Bluetoth LE +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ Luma Stiks +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+ +
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/index.html b/firmware_update/latest/data/www/index.html new file mode 100644 index 0000000..6c6f81a --- /dev/null +++ b/firmware_update/latest/data/www/index.html @@ -0,0 +1,29 @@ + + + + + ESP32 AP + + + +

Welcome to ESP32 AP

+

This is a simple web server running on ESP32 in Access Point mode.

+
+ + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/lights.html b/firmware_update/latest/data/www/lights.html new file mode 100644 index 0000000..01a004b --- /dev/null +++ b/firmware_update/latest/data/www/lights.html @@ -0,0 +1,527 @@ + + + + + + + + + + + + + +
+

Lights Configuration

+
+ Countdown ( Constant Light ) +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ +      + +
+
+
+
+ +
+
+ Saved Animation Profiles +
+
+ + +
+
+ + + +
+
+
+
+ +
+ + + + diff --git a/firmware_update/latest/data/www/navbar.html b/firmware_update/latest/data/www/navbar.html new file mode 100644 index 0000000..406a174 --- /dev/null +++ b/firmware_update/latest/data/www/navbar.html @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/firmware_update/latest/data/www/ok.html b/firmware_update/latest/data/www/ok.html new file mode 100644 index 0000000..6df4dda --- /dev/null +++ b/firmware_update/latest/data/www/ok.html @@ -0,0 +1,34 @@ + + + + Update Success + + + + + + + +
+

The update was successful.

+
+ +
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/setup.html b/firmware_update/latest/data/www/setup.html new file mode 100644 index 0000000..18b6fb9 --- /dev/null +++ b/firmware_update/latest/data/www/setup.html @@ -0,0 +1,385 @@ + + + + Booth Configuration Tools + + + + + + + + +

System Setup

+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ Test Tool (Single LED) + +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ Luma Stiks Found + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+ + +
+
+
+ + +
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/upgrade.html b/firmware_update/latest/data/www/upgrade.html new file mode 100644 index 0000000..f7da5e9 --- /dev/null +++ b/firmware_update/latest/data/www/upgrade.html @@ -0,0 +1,234 @@ + + + + + + Firmware Upgrade + + + + +
+

Firmware Upgrade

+ +
+ + Disconnected +
+ +
+

Current Version: -

+

Latest Version: -

+
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/firmware_update/latest/data/www/wifi.html b/firmware_update/latest/data/www/wifi.html new file mode 100644 index 0000000..06e3df9 --- /dev/null +++ b/firmware_update/latest/data/www/wifi.html @@ -0,0 +1,159 @@ + + + + + + Wifi Access + + + + + +

Wifi Access

+
+
+ + +
+ + + + + +
+ + + + diff --git a/firmware_update/latest/firmware.bin b/firmware_update/latest/firmware.bin new file mode 100644 index 0000000..1422fc8 Binary files /dev/null and b/firmware_update/latest/firmware.bin differ diff --git a/firmware_update/latest/update.json b/firmware_update/latest/update.json new file mode 100644 index 0000000..4343b57 --- /dev/null +++ b/firmware_update/latest/update.json @@ -0,0 +1,164 @@ +{ + "version": { + "major": 1, + "minor": 4, + "patch": 5 + }, + "firmware": { + "file": "firmware.bin", + "md5": "b8c880418a180efb23260ee093d13e61", + "size": 1409568 + }, + "files": [ + { + "remote": "data/ata-boothifier-upgrade.html", + "local": "/ata-boothifier-upgrade.html", + "md5": "e452db020a5ad660599129ab35e01058", + "size": 16736 + }, + { + "remote": "data/favicon.ico", + "local": "/favicon.ico", + "md5": "ba4c4e3bf5e5db2bbfc56a52f3657d79", + "size": 1150 + }, + { + "remote": "data/css/global-style.css", + "local": "/css/global-style.css", + "md5": "217a9cca8b4eae2d28fa8bc5a0f6db09", + "size": 1239 + }, + { + "remote": "data/css/nav.css", + "local": "/css/nav.css", + "md5": "e653a75433056f28e3f410f567b8622c", + "size": 1538 + }, + { + "remote": "data/images/atalogo.png", + "local": "/images/atalogo.png", + "md5": "5a18c88a4ea80c8d8d0ad52b2fdbbadc", + "size": 16690 + }, + { + "remote": "data/images/favicon-32x32.png", + "local": "/images/favicon-32x32.png", + "md5": "d80cf74ace3a8be487a5158bca48f9b6", + "size": 2428 + }, + { + "remote": "data/js/event-box.js", + "local": "/js/event-box.js", + "md5": "553f26707686038e275227a97eb96659", + "size": 12341 + }, + { + "remote": "data/js/fwUoload.js", + "local": "/js/fwUoload.js", + "md5": "d4a734cce529c3adf831ea46259f9c2d", + "size": 1524 + }, + { + "remote": "data/js/hue-select.js", + "local": "/js/hue-select.js", + "md5": "0a58f1a339af5c54aecfc5ce1aac7168", + "size": 6367 + }, + { + "remote": "data/js/jquery-3.7.1.js", + "local": "/js/jquery-3.7.1.js", + "md5": "fdb81281d3773a7462998fdddbe6f5bf", + "size": 196885 + }, + { + "remote": "data/system/tunes.json", + "local": "/system/tunes.json", + "md5": "814999e88296bee179cef5d394f3f696", + "size": 1149 + }, + { + "remote": "data/system/update.json", + "local": "/system/update.json", + "md5": "8046dbf9490cfd88fe5970b4ec1ac2cd", + "size": 91 + }, + { + "remote": "data/www/about.html", + "local": "/www/about.html", + "md5": "23f0c991c5c67e7eefe6c24aa50b59fb", + "size": 4222 + }, + { + "remote": "data/www/edit.html", + "local": "/www/edit.html", + "md5": "fed238dcf87d3797b08ed371330ea800", + "size": 5546 + }, + { + "remote": "data/www/edit_old.html", + "local": "/www/edit_old.html", + "md5": "9aafba533ac77352d30841cdc34db0c4", + "size": 4240 + }, + { + "remote": "data/www/failed.html", + "local": "/www/failed.html", + "md5": "a24025d56bef1cd2ed5375f6e8853dde", + "size": 828 + }, + { + "remote": "data/www/files.html", + "local": "/www/files.html", + "md5": "52d82d4d23b038929691cd0fb20e8ee0", + "size": 9369 + }, + { + "remote": "data/www/home.html", + "local": "/www/home.html", + "md5": "767fd73b733d8e37dc79252fcd20b610", + "size": 7822 + }, + { + "remote": "data/www/index.html", + "local": "/www/index.html", + "md5": "af690aaa4dec02691ffdb2765c837370", + "size": 806 + }, + { + "remote": "data/www/lights.html", + "local": "/www/lights.html", + "md5": "272f8dc423923bb5026837240f654efe", + "size": 19056 + }, + { + "remote": "data/www/navbar.html", + "local": "/www/navbar.html", + "md5": "89af40b990e297e41e116105b04b66ee", + "size": 533 + }, + { + "remote": "data/www/ok.html", + "local": "/www/ok.html", + "md5": "db42bd2ff52293c3b4d6ef62fb4e3a42", + "size": 865 + }, + { + "remote": "data/www/setup.html", + "local": "/www/setup.html", + "md5": "19e1f419844852800757ccd36bb7892a", + "size": 11349 + }, + { + "remote": "data/www/upgrade.html", + "local": "/www/upgrade.html", + "md5": "f4a3efb67be66b5214d905e605984c93", + "size": 9080 + }, + { + "remote": "data/www/wifi.html", + "local": "/www/wifi.html", + "md5": "c6e55946a02cb0ff28689dd10d265987", + "size": 5320 + } + ] +} \ No newline at end of file diff --git a/firmware_update/loyal-column-439819-e3-8cddff2ee2c2.json b/firmware_update/loyal-column-439819-e3-8cddff2ee2c2.json new file mode 100644 index 0000000..a012359 --- /dev/null +++ b/firmware_update/loyal-column-439819-e3-8cddff2ee2c2.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "loyal-column-439819-e3", + "private_key_id": "8cddff2ee2c2b8fb9c895cc02832cace7bec8dfc", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDKBvkZwnQe+yRJ\nMnaDYqMYaYsmziC74D4cdIOhLcLfUTB9r+VX0YwGuPEDvqqRKfEt57pxN4U+pZEz\nIt+P5J/kKQ2S9mBd5n5/YK5mgbGpErvV1OSBX/L01lW9LD3rEh3HqVcSPtqToquV\n9MuERSdQ9HoPfaWRKVxMYY5lrmericS9p0OndjoR99rTTkwvY0baSRBEL5lsFpaX\neuzVNxtQPe3uGBAbNIROoIgV+2zMC0lEu5IkqAfxhPmwYbBxIz39l4Z+lSMjnc6y\na5O37VYId8yc2UEZVpVfQrFzRX5bs5oq4DL5s6RQAcLsjHEBc8IDCWKn9iZpLQoG\ndVPLAHz1AgMBAAECggEAMtARLDDz6uzowf5Ov5n6AoRdSuvSYN60UIzfpJTxbCCs\nDRjGGtHqIbC3ceUtWmiNsCmWA67etRSIki+FwlfS/Vxz/RA5ybRVAa2r/71EFY3Q\nL6aluIKNBZa5JuTvR+YdFmBZtY2YhUSHWqag34pTKrHq12WXVIzuGYn5+kok6ghA\nyGLBDRlrhMwmTR14N6bEG/I81T+b+Ms73nvsA5OzmOFg0ZB+2DmYM9qS0lXV/RbI\n3eptVJbFAvrjqnQN3G4Gzh61EJ+b2FtFqQNFSNpH6YKETfW4kCFS0wz96xvsQ1I4\nNgb+CZSUwzXSlTIfbAs2eeMdgPhclk47QQK3aliKWwKBgQDnaqwQeJ0oy4FagcfC\nCc+4CoqIpAhxxgepztjaK3nKaZW+qnIxLTm+ysUJ1OYy6O1aFiBT/SID3HtQQkXm\n6E876Uth2EWVQ0JBWlNtyzahteSgfDyLxDD1vhqdKOt6YIiB1CMJu0gmN1YAXINh\n3UOJmvf3SMeSJfop9LqOSTFQbwKBgQDffQ+hq6kjUYqaq3+TpFSPtX/xzyBbVhZu\ndicjyxySHPHh5CsOO45oRwT5EIQaNqj1OGrQdf+cot2PaTCN/Xcfi+YmeiWFWcBa\nj+eoH80EkqXX173lpKJ/q1j4Z02ju9OkLFo+xKpPEJ/tyMQJ+Y32lf+lcJAYbQ6G\nWEE+vRUy2wKBgEJADaZhgUyOhietA80PFgJwMhzQKQK5WLRKXVaIH8PM2mvNTAe8\nFM4EKsiXxJdKi7jAoNyqmTRVyI+/iVeDpjtWnJgUXEdRDAS9oSjLhoZhFmTDbGnu\n7r1LgT8A2fkc7beNTcKZdRxLtzk/QvWfbJhWXyrSESBL8wRT5ZyaY53NAoGASo8G\ncAE85DOH8iHU5gSk0WzTynA3c0E9KozwcdiJJ5XQfHQKiS0FTXSkDBOefsBNjcHM\noM88/5y2HeoR8MzmuPeLSLrnWZ2ftpbbyhMR6ryh52hqSfqq27zmabjNBhrbeKHz\nWY1y4/2/3Sxleo5u9h0EtMTgzcVUWy2GVs/uCpECgYASxJ1x3Bw3W6DnXhU2UfRJ\nSC5hdxnaWUqvmZmtRIsVzxgg/ZpAPY5cefhLUz70KuLcFF0GOrYUOKrknsHtHinq\nL/+E3IRtfHzQOabR5uB4WFNodJ9cr+pSIiCtC9FxUWaO2xpa4xr2wC04qfQyhhb1\n0x1gjcafgbY8FPIbKdS/1A==\n-----END PRIVATE KEY-----\n", + "client_email": "boothifierservice@loyal-column-439819-e3.iam.gserviceaccount.com", + "client_id": "103041587650332588407", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/boothifierservice%40loyal-column-439819-e3.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/include/ATALights.h b/include/ATALights.h new file mode 100644 index 0000000..1004ca3 --- /dev/null +++ b/include/ATALights.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include "ColorPalettes.h" + +#define PIXEL_INDEX -3 +#define SOLID_COLOR_INDEX -2 +#define OFF_INDEX -1 + +typedef struct { + int AnimationIndex; + union { + struct { + uint8_t red; + uint8_t grn; + uint8_t blu; + uint8_t wht; + uint8_t extra[4]; // 4 extra bytes + }; + uint8_t rgb[3]; // Access RGB only + uint8_t rgbw[4]; // Access RGBW + uint8_t data[8]; // Access all 8 bytes + } data; +} ANIM_EVENT; + +typedef struct { + CRGB* leds; + bool enabled; + int size; + String chip; + String rgbOrder; + int shift; + int offset; + int powerDiv; + int effSize; + uint8_t bright; + uint8_t i2sCh; + uint8_t core; + uint8_t pin; +}LEDSTRIP_SETTINGS; + +extern LEDSTRIP_SETTINGS ledSettings[2]; + +// Forward declarations +//extern CRGB* leds1; +//extern CRGB* leds2; + +void Lights_Control_Task(void *parameters); +void Init_Lights_Task(void); +void Lights_Control_Task_Resume(void); +void Init_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder, const String& chipType, uint8_t bright); + +EOrder GetEOrderByString(const String& rgbStr); + +void Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t blu); +void Lights_Set_ON(void); +void Lights_Set_OFF(void); +void Lights_Set_Brightness(uint8_t scale); +void Lights_Set_White(uint8_t val); +void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3); +void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src); + + + + + + + + + + + + + + + diff --git a/include/Animations.h b/include/Animations.h new file mode 100644 index 0000000..33690e5 --- /dev/null +++ b/include/Animations.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include "ColorPalettes.h" + +#define MinLoopDelay 2 +#define MaxLoopDelay 200 +#define MaxSpeed 100 + +#define RANDOM_DECAY true +#define LINEAR_DECAY false + +#define CYCLES_PER_DIRECTION 2 + +void Animation_Init(void); + +void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function callback); +void Animation_Loop_Limited(bool volatile& loop_active_flag, int speed, TickType_t duration, std::function callback); +void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function callback); + +void Anim_Rainbow(bool volatile& loop_active_flag, CRGB* leds, int size, int speed); + +void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const CRGBPalette16& firePalette, int shift); + +void Anim_Color_Sectors(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, uint8_t numRepeats, int speed); + +//template +//void Anim_Color_Gradient_Trans(bool volatile& activeFlag, CRGB* leds, int size, TPalette& palette, uint8_t paletteSize, uint8_t numRepeats, int speed); +//void Anim_Color_Gradient_Red_Yellow_Violet(bool volatile& activeFlag, CRGB* leds, int size, int speed); + +void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed, bool randomDecay, bool shorterTail, int cometMultiplier); +//void Anim_Comet_Rainbow(bool volatile& activeFlag, CRGB* leds, int size, int speed); + +void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB col1, CRGB col2, int totalDurationMs, int shift); + +void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, uint32_t timeMs, int speed); + +void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed); + +uint32_t getRandomValue(uint32_t maxValue); diff --git a/include/AppUpgrade.h b/include/AppUpgrade.h new file mode 100644 index 0000000..20d5516 --- /dev/null +++ b/include/AppUpgrade.h @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "AppVersion.h" + +#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/" +#define BUFFER_SIZE 4096 + +extern TaskHandle_t Update_Task_Handle; + +/** + * @brief Version information structure + */ + + +//bool versionCompare(Version& remoteVersion, Version& localVersion); + +extern Version otaVersion; + +/** + * @brief File information structure + */ +struct FileInfo { + String remotePath; ///< Path on remote server + String localPath; ///< Path in local filesystem + String md5; ///< MD5 hash for verification + //size_t size; ///< File size in bytes +}; + +/** + * @class AppUpdater + * @brief Handles firmware and filesystem updates + */ +class AppUpdater { + public: + Version localVersion; + Version otaVersion; + const char* bucketUrl; + const char* appName; + const char* manifestName; + JsonDocument jsonManifest; + JsonArray jsonFilesArray; + + /** + * @brief Update status enumeration + */ + enum class UpdateStatus { + IDLE, ///< No update in progress + MESSAGE, ///< Update message + DOWNLOADING, ///< Downloading files + VERIFYING, ///< Verifying file integrity + FILE_SKIPPED, ///< File already up to date + FILE_SAVED, ///< Saving to filesystem + MD5_FAILED, ///< MD5 verification failed + COMPLETE, ///< Update complete + ERROR ///< Error occurred + }; + + /** + * @brief Constructor + * @param version Current firmware version + * @param bucket Base URL for updates + * @param fs Filesystem reference + */ + AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName ="update.json", const char* appBin = "firmware.bin" ); + + /** + * @brief Set progress callback function + * @param callback Function to call with progress updates + */ + void setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)); + + /** + * @brief Check for and apply updates + * @return true if update successful + */ + bool IsUpdateAvailable(void); + + /** + * @brief Update files from manifest + * @param manifestPath Path to manifest file + * @return true if all files updated successfully + */ + //bool updateFilesFromManifest(const char* manifestPath = DEFAULT_MANIFEST_URL); + bool updateFilesArray(void); + + /** + * @brief Update single file + * @param remotePath Remote file path + * @param localPath Local file path + * @param expectedMd5 Expected MD5 hash + * @return true if file updated successfully + */ + bool updateFile(const char* remotePath, const char* localPath, const char* expectedMd5); + + /** + * @brief Get manifest content + * @param manifestPath Path to manifest file + * @return Manifest content as a json document + */ + bool checkManifest(void); + + bool updateApp(void); + + String getVersion(void); + + private: + typedef void (*ProgressCallback)(UpdateStatus status, int percentage, const char* message); + ProgressCallback progressCb; + fs::FS& fileSystem; + UpdateStatus status; + std::unique_ptr downloadBuffer; + bool updateAvailable = false; + + + + /** + * @brief Verify and save file + * @param stream Input stream + * @param contentLength Expected content length + * @param localPath Local file path + * @param expectedMd5 Expected MD5 hash + * @return true if successful + */ + bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength, + const char* localPath, const char* expectedMd5); + + /** + * @brief Update progress callback + * @param percentage Progress percentage + * @param newStatus Current status + */ + void updateProgress(UpdateStatus newStatus, int percentage, const char* message = nullptr); + + String getLocalMD5(const char* filePath); +}; + + + +// Queue handle for firmware update messages +extern QueueHandle_t updateMsgQueue; + +// Message structure for update progress +struct UpdateMessage { + String message; + bool complete; + int progress; +}; + +/** + * @brief Firmware update task + * @param param Task parameters + */ +void firmwareUpdateTask(void* param); + +void startFirmwareUpdateTask(AsyncEventSource* eventSrc); + +void loadUpdateJson(void); + +void updateProgress(AppUpdater::UpdateStatus status, int percentage, const char* message); + +void sendUpdateMessage(const char* message, bool complete, int progress); + +void handleUpdateProgress(AsyncWebServerRequest *request); + +bool checkManifest(Version& remoteVersion); + +void startVersionCheckTask(); + +void versionCheckTask(void* parameter); \ No newline at end of file diff --git a/include/AppVersion.h b/include/AppVersion.h new file mode 100644 index 0000000..ecdb755 --- /dev/null +++ b/include/AppVersion.h @@ -0,0 +1,47 @@ +#pragma once +#include + +struct Version { + byte parts[3]; ///< Version numbers: [0]=major, [1]=minor, [2]=patch] + + byte& major() { return parts[0]; } + byte& minor() { return parts[1]; } + byte& patch() { return parts[2]; } + + const byte& major() const { return parts[0]; } + const byte& minor() const { return parts[1]; } + const byte& patch() const { return parts[2]; } + + String toString() const { + return String(parts[0]) + "." + String(parts[1]) + "." + String(parts[2]); + } + + // Comparison operators + bool operator==(const Version& other) const { + return parts[0] == other.parts[0] && + parts[1] == other.parts[1] && + parts[2] == other.parts[2]; + } + + bool operator!=(const Version& other) const { + return !(*this == other); + } + + bool operator<(const Version& other) const { + return (parts[0] < other.parts[0]) || + (parts[0] == other.parts[0] && parts[1] < other.parts[1]) || + (parts[0] == other.parts[0] && parts[1] == other.parts[1] && parts[2] < other.parts[2]); + } + + bool operator>(const Version& other) const { + return other < *this; + } + + bool operator<=(const Version& other) const { + return !(*this > other); + } + + bool operator>=(const Version& other) const { + return !(*this < other); + } +}; diff --git a/include/BLE_SP110E.h b/include/BLE_SP110E.h new file mode 100644 index 0000000..db2a470 --- /dev/null +++ b/include/BLE_SP110E.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +// Define constants +#define GET_CHECK_DEVICE 0xD5 +#define GET_DEVICE_INFO 0x10 +#define SET_IC_MODEL 0x1C +#define SET_RGB_SEQUENCE 0x3C +#define SET_LED_NUM 0x2D +#define SET_DEVICE_NAME 0xBB +#define TURN_ON 0xAA +#define TURN_OFF 0xAB +#define SET_STATIC_COLOR 0x1E +#define SET_BRIGHT 0x2A +#define SET_WHITE 0x69 +#define SET_PRESET 0x2C +#define SET_SPEED 0x03 +#define SET_AUTO_MODE 0x06 + +// Initializes the BLE server, services, and advertising +void Init_BLE_SP110E(NimBLEServer* pServer); + +void sendToAllClients(const uint8_t *data, size_t len); + +void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar); + +void Init_BLE_LightStick_Client(); + +void BLE_LightStick_Client_Task(void *parameter); + + + diff --git a/include/BLE_UpdateService.h b/include/BLE_UpdateService.h new file mode 100644 index 0000000..6d77d94 --- /dev/null +++ b/include/BLE_UpdateService.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +void Init_UpgradeBLEService(NimBLEServer *pServer); + +void bleUpgrade_send_message(String s); \ No newline at end of file diff --git a/include/BleServer.h b/include/BleServer.h new file mode 100644 index 0000000..670790f --- /dev/null +++ b/include/BleServer.h @@ -0,0 +1,5 @@ +#pragma once +#include + + +void Init_BleServer(bool isSP110EActive, bool isUpgradeActive); \ No newline at end of file diff --git a/include/ColorPalettes.h b/include/ColorPalettes.h new file mode 100644 index 0000000..412df14 --- /dev/null +++ b/include/ColorPalettes.h @@ -0,0 +1,45 @@ +#pragma once + +#include + + +typedef struct { + uint8_t size; + CRGB col[8]; +} COLOR_PACK; + +// Sectors +const COLOR_PACK colorPack_RAINBOW PROGMEM = { 7, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Green, CRGB::Blue, CRGB::BlueViolet, CRGB::MediumVioletRed } }; +const COLOR_PACK colorPack_USA PROGMEM = { 3, { CRGB::Red, CRGB::White, CRGB::Blue } }; +const COLOR_PACK colorPack_MEXICO PROGMEM = { 3, { CRGB::Green, CRGB::White, CRGB::Red } }; +const COLOR_PACK colorPack_CANADA PROGMEM = { 2, { CRGB::Red, CRGB::White } }; +const COLOR_PACK colorPack_GERMANY PROGMEM = { 3, { CRGB::Black, CRGB::Red, CRGB::Yellow } }; + +// Single Colors +const COLOR_PACK colorPack_Single_Red PROGMEM = { 1, { CRGB::Red } }; +const COLOR_PACK colorPack_Single_Orange PROGMEM = { 1, { CRGB::OrangeRed } }; +const COLOR_PACK colorPack_Single_Yellow PROGMEM = { 1, { CRGB::Yellow } }; +const COLOR_PACK colorPack_Single_Green PROGMEM = { 1, { CRGB::Green } }; +const COLOR_PACK colorPack_Single_Blue PROGMEM = { 1, { CRGB::Blue } }; +const COLOR_PACK colorPack_Single_Viloet PROGMEM = { 1, { CRGB::DarkViolet } }; +const COLOR_PACK colorPack_Single_Magenta PROGMEM = { 1, { CRGB::Magenta } }; +const COLOR_PACK colorPack_Single_White PROGMEM = { 1, { CRGB::White } }; + +// Dashes +const COLOR_PACK colorPack_RedBlack PROGMEM = { 2, { CRGB::Red, CRGB::Black } }; +const COLOR_PACK colorPack_OrangeBlack PROGMEM = { 2, { CRGB::DarkOrange, CRGB::Black } }; +const COLOR_PACK colorPack_YellowBlack PROGMEM = { 2, { CRGB::Yellow, CRGB::Black } }; +const COLOR_PACK colorPack_GreenBlack PROGMEM = { 2, { CRGB::Green, CRGB::Black } }; +const COLOR_PACK colorPack_BlueBlack PROGMEM = { 2, { CRGB::Blue, CRGB::Black } }; +const COLOR_PACK colorPack_IndigoBlack PROGMEM = { 2, { CRGB::Indigo, CRGB::Black } }; +const COLOR_PACK colorPack_VioletBlack PROGMEM = { 2, { CRGB::MediumVioletRed, CRGB::Black } }; +const COLOR_PACK colorPack_WhiteBlack PROGMEM = { 2, { CRGB::White, CRGB::Black } }; + + +const COLOR_PACK fire PROGMEM = { 4, { CRGB::Red, CRGB::OrangeRed, CRGB::Yellow, CRGB::Black } }; + + + +void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette); + + diff --git a/include/JsonConstrain.h b/include/JsonConstrain.h new file mode 100644 index 0000000..dc3e2c7 --- /dev/null +++ b/include/JsonConstrain.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +template +T jsonConstrain(const char *tag, const JsonObject &jsonObject, const char *key, T min, T max, T def); +const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, const char *key, const char *def); +String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def); +bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def); + + +extern template int jsonConstrain(const char *, const JsonObject &, const char *, int, int, int); +extern template float jsonConstrain(const char *, const JsonObject &, const char *, float, float, float); diff --git a/include/OnEveryN.h b/include/OnEveryN.h new file mode 100644 index 0000000..a70f602 --- /dev/null +++ b/include/OnEveryN.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +// Generic class to handle intervals +template +class OnEveryN { +public: + OnEveryN() : lastTime(0) {} + + // Check if the interval has elapsed + bool ready() { + unsigned long currentTime = millis(); + if (currentTime - lastTime >= interval) { + lastTime = currentTime; + return true; + } + return false; + } + +private: + unsigned long lastTime; // Stores the last execution time +}; + +// Helper macros to generate unique names +#define CONCATENATE_DETAIL(x, y) x##y +#define CONCATENATE(x, y) CONCATENATE_DETAIL(x, y) +#define UNIQUE_NAME(base) CONCATENATE(base, __LINE__) + +// Macro for ON_EVERY_N_MILLISECONDS +#define ON_EVERY_N_MILLISECONDS(N) \ + static OnEveryN UNIQUE_NAME(__on_everyN_); \ + if (UNIQUE_NAME(__on_everyN_).ready()) + +// Macro for ON_EVERY_N_SECONDS +#define ON_EVERY_N_SECONDS(N) ON_EVERY_N_MILLISECONDS((N) * 1000) + diff --git a/include/PWM_Output.h b/include/PWM_Output.h new file mode 100644 index 0000000..935dc5f --- /dev/null +++ b/include/PWM_Output.h @@ -0,0 +1,42 @@ +#pragma once +#include + + +struct PWMOUT{ + float max; + bool visionCorrected; + int resolution; + int frequency; +}; + +class PWM_Output { + public: + float currDuty; + bool visionCorrected; + PWM_Output(int8_t pin, uint8_t ch, uint8_t res, uint32_t freq, float maxDuty, bool visionCorrected=false); + void setOutput(float duty); + void setFreq(uint32_t fq); + int linearizeOutput(float inp); + void setMaxDuty(float duty); + int getOutVal(void); + void setResolution(uint8_t res); + float getMaxDuty(void) { + return maxDuty; + } + + int currOutVal; + private: + uint8_t ch; + uint32_t freq; + uint8_t res; + + float maxDuty; + int msecRampRate; + + float standardFactor; + float visionFactor; + +}; + + + diff --git a/include/Ramp_Lights.h b/include/Ramp_Lights.h new file mode 100644 index 0000000..0979131 --- /dev/null +++ b/include/Ramp_Lights.h @@ -0,0 +1,29 @@ +#pragma once + +#include "PWM_Output.h" +#include "OneButton.h" + +enum RAMP_STATE { RampingUp = 0, RampingDown }; + +class RAMP_LIGHT { +public: + RAMP_LIGHT(OneButton* button, PWM_Output* pwmOutput, int min, int max, float step); + +private: + PWM_Output* pwmOutput; + OneButton* button; + int min; + int max; + float step; + float currentValue; + bool IsOn = false; + RAMP_STATE rampState; + int tickCount; + + void tick(); + void singleClick(); + void longPressStart(); + void longPressStop(); + void duringLongPress(); +}; + diff --git a/include/UriDecode.h b/include/UriDecode.h new file mode 100644 index 0000000..1e4a331 --- /dev/null +++ b/include/UriDecode.h @@ -0,0 +1,21 @@ +#pragma once + +String uriDecode(const String& input) { + String decoded = ""; + char a, b; + for (size_t i = 0; i < input.length(); i++) { + if ((input[i] == '%') && (i + 2 < input.length()) && + ((a = input[i + 1]) && (b = input[i + 2])) && + isxdigit(a) && isxdigit(b)) { + a = (a <= '9') ? a - '0' : (toupper(a) - 'A' + 10); + b = (b <= '9') ? b - '0' : (toupper(b) - 'A' + 10); + decoded += char((a << 4) | b); + i += 2; + } else if (input[i] == '+') { + decoded += ' '; + } else { + decoded += input[i]; + } + } + return decoded; +} \ No newline at end of file diff --git a/include/_FreeRTOSConfig.h b/include/_FreeRTOSConfig.h new file mode 100644 index 0000000..8ad85c2 --- /dev/null +++ b/include/_FreeRTOSConfig.h @@ -0,0 +1,30 @@ +#ifndef FREERTOS_CONFIG_H +#define FREERTOS_CONFIG_H + +#include // Required for compatibility with Arduino framework + +//#define configUSE_PREEMPTION 1 +//#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1 +//#define configCPU_CLOCK_HZ ( 240000000UL ) +//#define configTICK_RATE_HZ ( 1000 ) +//#define configMAX_PRIORITIES ( 25 ) +//#define configMINIMAL_STACK_SIZE ( 1024 ) +//#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 64 * 1024 ) ) +//#define configMAX_TASK_NAME_LEN ( 16 ) +//#define configUSE_TRACE_FACILITY 1 +//#define configUSE_STATS_FORMATTING_FUNCTIONS 1 +//#define configGENERATE_RUN_TIME_STATS 0 +//#define configUSE_16_BIT_TICKS 0 +//#define configIDLE_SHOULD_YIELD 1 + +// Additional configuration for debugging +//#define configCHECK_FOR_STACK_OVERFLOW 2 +//#define configUSE_MALLOC_FAILED_HOOK 1 +//#define configUSE_IDLE_HOOK 1 +//#define configUSE_TICK_HOOK 0 + +// Optional macros for runtime stats collection (if needed) +//#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() // No-op +//#define portGET_RUN_TIME_COUNTER_VALUE() xTaskGetTickCount() + +#endif /* FREERTOS_CONFIG_H */ \ No newline at end of file diff --git a/include/global.h b/include/global.h new file mode 100644 index 0000000..b81495d --- /dev/null +++ b/include/global.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include "appVersion.h" + +//#define FIRMWARE_VERSION "1.0.0" +#define FIRMWARE_VERSION_MAJOR 1 +#define FIRMWARE_VERSION_MINOR 4 +#define FIRMWARE_VERSION_PATCH 4 + +extern Version localVersion; + +enum COMM_MODE { COMM_WIFI_AP_BLE, COMM_WIFI_AP_CLIENT }; +extern enum COMM_MODE commMode; + +typedef struct{ + int count; + TaskHandle_t handle[16]; +}SYS_TASK_HANDLES; + +extern SYS_TASK_HANDLES TaskList; + +typedef struct{ + uint64_t chipMAC; + uint8_t macByte[6]; + String macStr; + //char chipID_2Hex[4]; + size_t app_partition_size; + size_t app_partition_free; +}CHIP_INFO; + +extern CHIP_INFO chipInfo; + +// Stack, Heap and CPU Reporting +//#define ENABLE_SYSTEM_STATS 0 + +int findUnusedLedcChannel(void); +//void read_system_settings(void); +//void read_app_events_settings(void); +//void read_animations_list(void); +void report_system_stats(void); +void print_chip_info(void); +void get_chip_mac(char* macStr, size_t size); +void print_ram_info(void); +//void Pulse_LED_Status(int mSecs); +void printTaskInfo(void); +void printTaskCPUUsage(TaskHandle_t xTask); +void printTaskStackWatermark(TaskHandle_t xTask); +void addTaskHandleToList(SYS_TASK_HANDLES &list); +String getMacAddress(void); +String macToStr(uint8_t* mac); +bool updateJsonDocument(JsonDocument& doc, const char* filePath); + +void Log_CPU_Load(void); + +void print_task_watermarks(void); + diff --git a/include/my_board.h b/include/my_board.h new file mode 100644 index 0000000..4b092d8 --- /dev/null +++ b/include/my_board.h @@ -0,0 +1,37 @@ +#pragma once +#include + +#define I2C_SDA1_Pin 1 +#define I2C_SCL1_Pin 2 + +//int linearizeLED(float inp); + +typedef struct{ + int8_t rgb1; + int8_t rgb2; + int8_t btn[3]; + int8_t buzzer; + int8_t touch[5]; + int8_t shield; + int8_t relay[4]; + int8_t stat[2]; + int8_t adc1; + int8_t oled_dc; + int8_t oled_rst; + int8_t oled_mosi; + int8_t oled_sck; + int8_t oled_cs; + int8_t ext[2]; + int8_t rf433tx; + int8_t rf433rx; +}BOARD_PINS; + +extern BOARD_PINS* thisBoardPins; +#define setStatusPin1(state) digitalWrite(thisBoardPins->stat[0], state); +#define setStatusPin2(state) digitalWrite(thisBoardPins->stat[1], state); + +void Load_Board_Pins(BOARD_PINS& boardPins, String& path); +void Init_Board_Basic(BOARD_PINS& boardPins); +void updateFanControl(float temperature); +void Initialize_Rear_Control(int relayIndex, int buttonIndex, int rampTime, int steps, float min, float max); + diff --git a/include/my_buttons.h b/include/my_buttons.h new file mode 100644 index 0000000..972b40c --- /dev/null +++ b/include/my_buttons.h @@ -0,0 +1,29 @@ +#pragma once + +#include "my_board.h" +#include "OneButton.h" + +extern OneButton *boardButtons[3]; + +#define Update_Buttons() boardButtons[1]->tick(); boardButtons[2]->tick(); boardButtons[3]->tick(); + +void Init_ButtonEvents(int8_t (&pin)[3]); + +void btn1_click(); +void btn1_doubleClick(); +void btn1_LongPressStart(); +void btn1_LongPressStop(); +void btn1_DuringLongPress(); + +void btn2_click(); +void btn2_doubleClick(); +void btn2_LongPressStart(); +void btn2_LongPressStop(); +void btn2_DuringLongPress(); + +void btn3_click(); +void btn3_doubleClick(); +void btn3_LongPressStart(); +void btn3_LongPressStop(); +void btn3_DuringLongPress(); + diff --git a/include/my_buzzer.h b/include/my_buzzer.h new file mode 100644 index 0000000..69aeae4 --- /dev/null +++ b/include/my_buzzer.h @@ -0,0 +1,41 @@ +#pragma once + +#include + + +typedef enum { + TUNE_BOOT, + TUNE_SHUTDOWN, + TUNE_CONNECTED, + TUNE_DISCONNECTED, + TUNE_DONE, + TUNE_WARNING, + TUNE_ERROR, + TUNE_BLIP, + TUNE_THUMP, + TUNE_ACK, + TUNE_WAITING, + TUNE_LOWBEEP, + TUNE_HIGHBEEP +}TUNE_TYPE; + +//Tunes +typedef struct { + bool enabled; + int cycles; + int pause; + String melody; +}BUZZ_TUNE; + +#define TUNE_MAX_COUNT 12 + +extern BUZZ_TUNE buzzTune[TUNE_MAX_COUNT]; + +void Init_Buzzer(int8_t, const char* configPath); + +void Buzzer_Beep(int, int freq=1000); + +void Buzzer_Load_Tunes(const char* tunesPath); + +void Buzzer_Play_Tune(TUNE_TYPE, bool async=true, bool priority=true); + diff --git a/include/my_oled.h b/include/my_oled.h new file mode 100644 index 0000000..08fa290 --- /dev/null +++ b/include/my_oled.h @@ -0,0 +1,17 @@ +#pragma once +#include + +extern Adafruit_SSD1306 *oled; + +void Init_OLED(uint8_t width, uint8_t height, uint8_t mosiPin, uint8_t sckPin, uint8_t dcPin, uint8_t rstPin, uint8_t csPin); + +void oled_ShowInfo(void); + +void testdrawline(Adafruit_SSD1306* display); // Draw many lines + +void testdrawrect(Adafruit_SSD1306* display); // Draw rectangles (outlines) + + + + + diff --git a/include/my_tsensor.h b/include/my_tsensor.h new file mode 100644 index 0000000..995007e --- /dev/null +++ b/include/my_tsensor.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "PWM_Output.h" + +typedef struct{ + bool enabled; + float temperature; + float Setpoint1; + float Setpoint2; + float fanPower1; + float fanPower2; + float hyst; +}T_SENSOR; + +extern T_SENSOR tSensorSettings; +extern TI_TMP102_Compatible *tSensor; + +void Init_TSensor(uint8_t addr); +void UpdateFanControl(float temperature, PWM_Output* pwmOut); + + + diff --git a/include/my_wifi.h b/include/my_wifi.h new file mode 100644 index 0000000..2745c16 --- /dev/null +++ b/include/my_wifi.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include + +extern volatile bool InternetAvailable; + +void Wifi_Init(void); +void Wifi_Load_Settings(String path); +void Wifi_Scan_for_Networks(void); +void Wifi_Start_MDNS(void); +void onWiFiEvent(WiFiEvent_t event); +bool Wifi_Save_Credentials(String path); +void Setup_WebServer_Handlers(AsyncWebServer& serv); + +void handlePOST_Update(AsyncWebServerRequest *request); +void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); +void updateFirmwareProgress(size_t progress, size_t total); + +void handleGET_Query(AsyncWebServerRequest *request); + +void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)); +String fileManagerHtmlProcessor(const String& var); +String HomeHtmlProcessor(const String& var); +String listDirAsHtml(String directoryList[], int count); + +const char* getFileExtension(const char* filename); +const char* getFileType(const char* ext); +const char* convertFileSize(const size_t bytes); +bool writeFile(fs::FS &fs, const char *path, const char *message); +char* readFile(fs::FS &fs, const char *path); + +String varReplace(const String& input, String (*callback)(const String&)); +String getSoftAPMacAddress(void); + +void Wifi_ConnectTask(void* parameter); +void Wifi_Check_Internet(void); +bool StartWifiConnectTask(String ssid, String pass); diff --git a/include/system.h b/include/system.h new file mode 100644 index 0000000..ad74af9 --- /dev/null +++ b/include/system.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include "my_board.h" +#include +#include "ATALights.h" + +typedef struct { + bool enabled; +}BTN_SETTINGS; + +typedef struct{ + bool enabled; + int freq; + float min; + float max; + float def; + float deltaRate; +}PWM_OUT_SETTINGS; + +typedef struct{ + bool enabled; + bool vision; + uint8_t pwmOutIndex; + uint8_t btnIndex; + float min; + float max; + float step; + uint8_t skipCount; +}RAMP_LIGHT_SETTINGS; + +typedef struct { + bool enabled; + int height; + int width; +}OLED_SETTINGS; + +typedef struct { + bool enabled; + uint8_t addr; + float setpoint1; + float fanPower1; + float setpoint2; + float fanPower2; + float hyst; + uint8_t pwmIndex; + int intervalMs; +}TSENSOR_SETTINGS; + +typedef struct { + bool enabled; + String name; + uint8_t core; +}BLE_SETTINGS; + +typedef struct { + bool txEnabled; + bool rxEnabled; +}RF_REMOTE_SETTINGS; + +typedef struct { + bool enabled; + String ssid; + String pass; +}WIFI_CLIENT_SETTINGS; + +typedef struct { + bool enabled; + String ssid; + String pass; + uint8_t ip[4]; + uint8_t gateway[4]; + uint8_t subnet[4]; +}WIFI_AP_SETTINGS; + +typedef struct { + bool enabled; +}ADC_SETTINGS; + +enum BOOTH_MODE { BOOTH_MODE_NONE=0, BOOTH_MODE_ROAMER=1, BOOTH_MODE_STIK=2 }; +typedef struct { + BOOTH_MODE mode; + BOARD_PINS boardPins; + BTN_SETTINGS btnSettings[3]; + PWM_OUT_SETTINGS pwmOutSettings[4]; + RAMP_LIGHT_SETTINGS rampLightSettings[2]; + OLED_SETTINGS oledSettings; + TSENSOR_SETTINGS tSensorSettings; + LEDSTRIP_SETTINGS* ledStripSettings[2]; + BLE_SETTINGS bleSettings; + RF_REMOTE_SETTINGS rfRemoteSettings; +}SYS_SETTINGS; + + diff --git a/lib/AnyRtttl/.github/workflows/build_linux.yml b/lib/AnyRtttl/.github/workflows/build_linux.yml new file mode 100644 index 0000000..8aa6d36 --- /dev/null +++ b/lib/AnyRtttl/.github/workflows/build_linux.yml @@ -0,0 +1,182 @@ +name: Linux + +on: [push, pull_request] + +env: + # build Configuration, i.e. Debug, Release, etc. + PRODUCT_BUILD_TYPE: Release + + # build Configuration, i.e. Debug, Release, etc. + Configuration: Release + + # Required for Github Action. Unit test TestProcess.testKillAndTerminate fails to start /bin/nano with the following error: "Error opening terminal: unknown." + TERM: xterm + +jobs: + build: + # For a list of available runner types, refer to + # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Configure GIT + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + git config --local core.autocrlf true + git config --local advice.detachedHead false + git submodule update --init --recursive + + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + + - name: Create new environment variables + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + python -c "import os; print('GIT_REPOSITORY=' + os.path.split(os.getenv('GITHUB_REPOSITORY'))[1]);" >> $GITHUB_ENV + python -c "import os; print('GIT_BRANCH=' + os.path.split(os.getenv('GITHUB_REF'))[1]);" >> $GITHUB_ENV + echo GITHUB_WORKFLOW=$GITHUB_WORKFLOW>> $GITHUB_ENV + + - name: List environment variables for debugging + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + env + + - name: Configure user profile and directories + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + mkdir ~/Documents #required for TestUser.testFoldersExisting() because /home/travis/Documents does not exists! + mkdir ~/Desktop #required for TestUser.testFoldersExisting() because /home/travis/Desktop does not exists! + + - name: Deploy 'tests not available' badge before building + uses: exuanbo/actions-deploy-gist@v1 + with: + token: ${{ secrets.BADGES }} + gist_id: 58cf6c72c08e706335337d5ef9ca48e8 + gist_file_name: ${{env.GIT_REPOSITORY}}.${{env.GIT_BRANCH}}.${{env.GITHUB_WORKFLOW}}.json + file_path: ./ci/github/tests_not_available.badge.json + + - name: Install Arduino CLI + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/install_arduinocli.sh + + - name: Install Arduino library dependnecies + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_install_libraries.sh + + - name: Install this arduino library + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/install_this.sh + + - name: Install Google Test + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/install_googletest.sh + + - name: Install RapidAssist + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/install_rapidassist.sh + + - name: Install win32arduino + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/install_win32arduino.sh + + - name: Build library + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/build_library.sh + + - name: Build Arduino sketch - Basic + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh Basic + + - name: Build Arduino sketch - BlockingProgramMemoryRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh BlockingProgramMemoryRtttl + + - name: Build Arduino sketch - BlockingRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh BlockingRtttl + + - name: Build Arduino sketch - BlockingWithNonBlocking + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh BlockingWithNonBlocking + + - name: Build Arduino sketch - NonBlockingProgramMemoryRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh NonBlockingProgramMemoryRtttl + + - name: Build Arduino sketch - NonBlockingRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh NonBlockingRtttl + + - name: Build Arduino sketch - NonBlockingStopBeforeEnd + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh NonBlockingStopBeforeEnd + + - name: Build Arduino sketch - Play10Bits + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh Play10Bits + + - name: Build Arduino sketch - Play16Bits + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh Play16Bits + + - name: Build Arduino sketch - Rtttl2Code + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/arduino_build_sketch.sh Rtttl2Code + + - name: Run unit tests + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: ./ci/github/test_script.sh + + - name: Search unit test report file + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: bash + run: | + UNITTEST_REPORT_PATH=$(find . -name 'anyrtttl_unittest*.xml') + echo UNITTEST_REPORT_PATH=$UNITTEST_REPORT_PATH + echo UNITTEST_REPORT_PATH=$UNITTEST_REPORT_PATH >> $GITHUB_ENV + + - name: Create test badge + working-directory: ${{env.GITHUB_WORKSPACE}} + run: python ci/github/maketestbadge.py ${{env.UNITTEST_REPORT_PATH}} + + - name: Deploy test badge to gist + uses: exuanbo/actions-deploy-gist@v1 + with: + token: ${{ secrets.BADGES }} + gist_id: 58cf6c72c08e706335337d5ef9ca48e8 + gist_file_name: ${{env.GIT_REPOSITORY}}.${{env.GIT_BRANCH}}.${{env.GITHUB_WORKFLOW}}.json + file_path: ./badge.json + + - name: Archive test results + uses: actions/upload-artifact@v3 + with: + name: unit-test-results + path: build/bin/anyrtttl_unittest*.xml diff --git a/lib/AnyRtttl/.github/workflows/build_windows.yml b/lib/AnyRtttl/.github/workflows/build_windows.yml new file mode 100644 index 0000000..18a9db6 --- /dev/null +++ b/lib/AnyRtttl/.github/workflows/build_windows.yml @@ -0,0 +1,174 @@ +name: Windows + +on: [push, pull_request] + +env: + PlatformToolset: v142 + + # build platform, i.e. x86, x64, Any CPU. This setting is optional. + Platform: Win32 + + # build Configuration, i.e. Debug, Release, etc. + Configuration: Release + +jobs: + build: + # For a list of available runner types, refer to + # https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idruns-on + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Configure GIT + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: | + git config --local core.autocrlf true + git config --local advice.detachedHead false + git submodule update --init --recursive + + - name: Setup python + uses: actions/setup-python@v3 + with: + python-version: '3.x' # Version range or exact version of a Python version to use, using SemVer's version range syntax + architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified + + - name: Create new environment variables + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: | + python -c "import os; print('GIT_REPOSITORY=' + os.path.split(os.getenv('GITHUB_REPOSITORY'))[1]);" >> %GITHUB_ENV% + python -c "import os; print('GIT_BRANCH=' + os.path.split(os.getenv('GITHUB_REF'))[1]);" >> %GITHUB_ENV% + echo GITHUB_WORKFLOW=%GITHUB_WORKFLOW%>> %GITHUB_ENV% + + - name: List environment variables for debugging + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: | + set + + - name: Deploy 'tests not available' badge before building + uses: exuanbo/actions-deploy-gist@v1 + with: + token: ${{ secrets.BADGES }} + gist_id: 58cf6c72c08e706335337d5ef9ca48e8 + gist_file_name: ${{env.GIT_REPOSITORY}}.${{env.GIT_BRANCH}}.${{env.GITHUB_WORKFLOW}}.json + file_path: ./ci/github/tests_not_available.badge.json + + - name: Install Arduino CLI + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\install_arduinocli.bat + + - name: Install Arduino library dependnecies + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_install_libraries.bat + + - name: Install this arduino library + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\install_this.bat + + - name: Install Google Test + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\install_googletest.bat + + - name: Install RapidAssist + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\install_rapidassist.bat + + - name: Install win32arduino + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\install_win32arduino.bat + + - name: Build library + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\build_library.bat + + - name: Build Arduino sketch - Basic + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat Basic + + - name: Build Arduino sketch - BlockingProgramMemoryRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat BlockingProgramMemoryRtttl + + - name: Build Arduino sketch - BlockingRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat BlockingRtttl + + - name: Build Arduino sketch - BlockingWithNonBlocking + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat BlockingWithNonBlocking + + - name: Build Arduino sketch - NonBlockingProgramMemoryRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat NonBlockingProgramMemoryRtttl + + - name: Build Arduino sketch - NonBlockingRtttl + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat NonBlockingRtttl + + - name: Build Arduino sketch - NonBlockingStopBeforeEnd + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat NonBlockingStopBeforeEnd + + - name: Build Arduino sketch - Play10Bits + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat Play10Bits + + - name: Build Arduino sketch - Play16Bits + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat Play16Bits + + - name: Build Arduino sketch - Rtttl2Code + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\arduino_build_sketch.bat Rtttl2Code + + - name: Run unit tests + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: call ci\github\test_script.bat + + - name: Search unit test report file + working-directory: ${{env.GITHUB_WORKSPACE}} + shell: cmd + run: | + for /f "tokens=* usebackq" %%f in (`dir/b/s build\bin\${{env.configuration}}\anyrtttl_unittest*.xml`) do (set UNITTEST_REPORT_PATH=%%f) + echo UNITTEST_REPORT_PATH=%UNITTEST_REPORT_PATH% + echo UNITTEST_REPORT_PATH=%UNITTEST_REPORT_PATH% >> %GITHUB_ENV% + + - name: Create test badge + working-directory: ${{env.GITHUB_WORKSPACE}} + run: python ci\github\maketestbadge.py ${{env.UNITTEST_REPORT_PATH}} + + - name: Deploy test badge to gist + uses: exuanbo/actions-deploy-gist@v1 + with: + token: ${{ secrets.BADGES }} + gist_id: 58cf6c72c08e706335337d5ef9ca48e8 + gist_file_name: ${{env.GIT_REPOSITORY}}.${{env.GIT_BRANCH}}.${{env.GITHUB_WORKFLOW}}.json + file_path: ./badge.json + + - name: Archive test results + uses: actions/upload-artifact@v3 + with: + name: unit-test-results + path: build\bin\${{env.Configuration}}\anyrtttl_unittest*.xml diff --git a/lib/AnyRtttl/.gitignore b/lib/AnyRtttl/.gitignore new file mode 100644 index 0000000..a874d7b --- /dev/null +++ b/lib/AnyRtttl/.gitignore @@ -0,0 +1,4 @@ +/build +/external +/third_parties +.vscode diff --git a/lib/AnyRtttl/.piopm b/lib/AnyRtttl/.piopm new file mode 100644 index 0000000..1e0f6b1 --- /dev/null +++ b/lib/AnyRtttl/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "AnyRtttl", "version": "2.3.0", "spec": {"owner": "end2endzone", "id": 8296, "name": "AnyRtttl", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/lib/AnyRtttl/AUTHORS b/lib/AnyRtttl/AUTHORS new file mode 100644 index 0000000..ef27329 --- /dev/null +++ b/lib/AnyRtttl/AUTHORS @@ -0,0 +1,5 @@ +# This file contains a list of people who've made contribution to +# the project. People who commit code are encouraged to add +# their names here. Please keep the list sorted by first names. + +Antoine Beauchamp diff --git a/lib/AnyRtttl/CHANGES b/lib/AnyRtttl/CHANGES new file mode 100644 index 0000000..491a02e --- /dev/null +++ b/lib/AnyRtttl/CHANGES @@ -0,0 +1,32 @@ +Changes for 2.3 + +* Fixed issue #2 - Support for PROGMEM / FLASH melodies in non-blocking mode. + + +Changes for 2.2.1: + +* Fixed issue #4: error compiling example code. +* Fixed issue #5: Refactor build process to use Arduino CLI instead of Arduino IDE. + + +Changes for 2.2.0: + +* New feature: Build option `ANYRTTTL_BUILD_EXAMPLES` to enable/disable building AnyRtttl examples. +* New feature: Changed file/folder structure to be compatible with Arduino Library Manager. +* New feature: Using RapidAssist 0.5.0 and win32Arduino 2.3.1. + + +Changes for 2.1.229: + +* New feature: Implemented support for RTTTL in Program Memory (PROGMEM). + + +Changes for 2.0.179: + +* Library converted to AnyRtttl. +* First github release. +* Code originally release at http://www.end2endzone.com/anyrtttl-a-feature-rich-arduino-library-for-playing-rtttl-melodies/ + + +Changes for 1.0.0: +* Initial release of NonBlockingRtttl. diff --git a/lib/AnyRtttl/CMakeLists.txt b/lib/AnyRtttl/CMakeLists.txt new file mode 100644 index 0000000..1e51d78 --- /dev/null +++ b/lib/AnyRtttl/CMakeLists.txt @@ -0,0 +1,160 @@ +cmake_minimum_required(VERSION 3.4.3) +project(AnyRtttl) + +# Set the output folder where your program will be created +set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) +set( LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin) + +############################################################################################################################################## +# Functions +############################################################################################################################################## +function(GIT_EXTERNAL DIR REPO_URL TAG) + # Find the name of the repository + get_filename_component(REPO_NAME ${REPO_URL} NAME_WE) + + # Compute output directory + set(REPO_DIR "${DIR}/${REPO_NAME}") + + if (EXISTS "${REPO_DIR}") + message(STATUS "Repository ${REPO_NAME} already exists in directory ${DIR}. Skipping git clone command.") + return() + endif() + + # Clone the repository + message(STATUS "git clone ${REPO_URL} ${REPO_DIR}") + execute_process( + COMMAND "${GIT_EXECUTABLE}" clone ${REPO_URL} ${REPO_DIR} + RESULT_VARIABLE returncode ERROR_VARIABLE error + WORKING_DIRECTORY "${DIR}") + if(returncode) + message(FATAL_ERROR "Clone failed: ${error}\n") + endif() + message(STATUS "git clone completed") + + # Checking out the required tag + message(STATUS "git checkout ${TAG}") + execute_process( + COMMAND "${GIT_EXECUTABLE}" checkout ${TAG} + RESULT_VARIABLE returncode ERROR_VARIABLE error + WORKING_DIRECTORY "${REPO_DIR}") + if(returncode) + message(FATAL_ERROR "Checkout failed: ${error}\n") + endif() + message(STATUS "git checkout completed") + + # Delete the .git folder to simulate an export of the repository + message(STATUS "Deleting ${REPO_DIR}/.git") + file(REMOVE_RECURSE "${REPO_DIR}/.git") + +endfunction() + +function(add_example name) + # Create custom example.cpp file which includes the ino sketch file. + SET(SOURCE_INO_FILE "${PROJECT_SOURCE_DIR}/examples/${name}/${name}.ino") + CONFIGURE_FILE("${CMAKE_CURRENT_SOURCE_DIR}/examples.cpp.in" "${PROJECT_BINARY_DIR}/${name}/examples.cpp") + + add_executable(${name} + ${ARDUINO_LIBRARY_SOURCE_FILES} + ${SOURCE_INO_FILE} + "${PROJECT_BINARY_DIR}/${name}/examples.cpp" + ) + + target_include_directories(${name} PRIVATE ${PROJECT_SOURCE_DIR}/src ${BITREADER_SOURCE_DIR} win32arduino ) + target_link_libraries(${name} PRIVATE win32arduino rapidassist) + + set_property(GLOBAL PROPERTY USE_FOLDERS ON) + set_target_properties(${name} PROPERTIES FOLDER "examples") + + if(WIN32) + # 1>C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\ostream(743,1): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc + # 1>D:\dev\AnyRtttl\master\third_parties\win32Arduino\install\include\win32arduino-2.4.0\SerialPrinter.h(202): message : see reference to function template instantiation 'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)' being compiled + set_target_properties(${name} PROPERTIES COMPILE_FLAGS "/wd4530") + endif() + +endfunction() + +############################################################################################################################################## +# Dependencies +############################################################################################################################################## +find_package(GTest REQUIRED) +find_package(rapidassist 0.5.0 REQUIRED) +find_package(win32arduino 2.3.1 REQUIRED) +find_package(Git REQUIRED) + +# Arduino BitReader library dependency +file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/external") +GIT_EXTERNAL("${CMAKE_CURRENT_SOURCE_DIR}/external" "http://github.com/end2endzone/BitReader.git" "1.3.0") +set(BITREADER_SOURCE_DIR "${PROJECT_SOURCE_DIR}/external/BitReader/src") + +############################################################################################################################################## +# Project settings +############################################################################################################################################## + +# Build options +option(ANYRTTTL_BUILD_EXAMPLES "Build all example projects" OFF) + +# Prevents annoying warnings on MSVC +if (WIN32) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) +endif() + +# Find all library source and unit test files +file( GLOB ARDUINO_LIBRARY_SOURCE_FILES ${PROJECT_SOURCE_DIR}/src/*.cpp ${PROJECT_SOURCE_DIR}/src/*.h ${BITREADER_SOURCE_DIR}/*.cpp ${BITREADER_SOURCE_DIR}/*.h) +file( GLOB ARDUINO_LIBRARY_TEST_FILES ${PROJECT_SOURCE_DIR}/test/*.cpp ${PROJECT_SOURCE_DIR}/test/*.h ) + +# Create unit test executable +add_executable(anyrtttl_unittest + ${ARDUINO_LIBRARY_SOURCE_FILES} + ${ARDUINO_LIBRARY_TEST_FILES} +) + +#include directories +target_include_directories(anyrtttl_unittest + PRIVATE ${PROJECT_SOURCE_DIR}/src # Arduino Library folder + ${GTEST_INCLUDE_DIR} + ${BITREADER_SOURCE_DIR} + win32arduino +) + +# Unit test projects requires to link with pthread if also linking with gtest +if(NOT WIN32) + set(PTHREAD_LIBRARIES -pthread) +endif() + +#link libraries +target_link_libraries(anyrtttl_unittest PRIVATE win32arduino rapidassist ${PTHREAD_LIBRARIES} ${GTEST_LIBRARIES} ) + +if(WIN32) + # 1>C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include\ostream(743,1): warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc + # 1>D:\dev\AnyRtttl\master\third_parties\win32Arduino\install\include\win32arduino-2.4.0\SerialPrinter.h(202): message : see reference to function template instantiation 'std::basic_ostream> &std::operator <<>(std::basic_ostream> &,const char *)' being compiled + set_target_properties(anyrtttl_unittest PROPERTIES COMPILE_FLAGS "/wd4530") +endif() + +# Copy `expected_call_stack.log` to expected locations +configure_file(${PROJECT_SOURCE_DIR}/test/expected_call_stack.log ${PROJECT_BINARY_DIR}/expected_call_stack.log COPYONLY) +set(TEST_FILES_BINARY_DIR ${EXECUTABLE_OUTPUT_PATH}) +if(WIN32) + set(TEST_FILES_BINARY_DIR ${TEST_FILES_BINARY_DIR}/${CMAKE_CFG_INTDIR}) +endif() +add_custom_command( + TARGET anyrtttl_unittest POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + ${PROJECT_SOURCE_DIR}/test/expected_call_stack.log + ${TEST_FILES_BINARY_DIR}/expected_call_stack.log) + +############################################################################################################################################## +# Add all samples to the project unless the user has specified otherwise. +############################################################################################################################################## +if(ANYRTTTL_BUILD_EXAMPLES) + add_example("Basic") + add_example("BlockingProgramMemoryRtttl") + add_example("BlockingRtttl") + add_example("BlockingWithNonBlocking") + add_example("NonBlockingProgramMemoryRtttl") + add_example("NonBlockingRtttl") + add_example("NonBlockingStopBeforeEnd") + add_example("Play10Bits") + add_example("Play16Bits") + add_example("Rtttl2Code") +endif() + \ No newline at end of file diff --git a/lib/AnyRtttl/INSTALL.md b/lib/AnyRtttl/INSTALL.md new file mode 100644 index 0000000..0174011 --- /dev/null +++ b/lib/AnyRtttl/INSTALL.md @@ -0,0 +1,105 @@ +# Install # + +The library can be found, installed, or updated from the Arduino IDE using the official Arduino Library Manager (available from IDE version 1.6.2). + + +The library can be installed on the system by following the same steps as with other Arduino library. + +Refer to [Installing Additional Arduino Libraries](https://www.arduino.cc/en/Guide/Libraries) tutorial for details on how to install a third party library. + + + + +# Dependencies # + +The AnyRtttl library have no dependencies on other Arduino library. However, some examples of the library requires external dependencies. + +The following Arduino Library must be installed on the system to use the library examples: + +* [BitReader v1.3.0](https://github.com/end2endzone/BitReader/tree/1.3.0) + + + + +# Build # + +The library unit tests can be build on Windows/Linux platform to maintain the product stability and level of quality. + +This section explains how to compile and build the software and how to get a test environment ready. + + + +## Prerequisites ## + + + +### Software Requirements ### + +The following software must be installed on the system before compiling unit test source code: + +* [Google C++ Testing Framework v1.8.0](https://github.com/google/googletest/tree/release-1.8.0) +* [RapidAssist v0.5.0](https://github.com/end2endzone/RapidAssist/tree/0.5.0) +* [win32Arduino v2.3.1](https://github.com/end2endzone/win32Arduino/tree/2.3.1) +* [CMake](http://www.cmake.org/) v3.4.3 (or newer) + + + +### Linux Requirements ### + +These are the base requirements to build source code: + + * GNU-compatible Make or gmake + * POSIX-standard shell + * A C++98-standard-compliant compiler + + + +### Windows Requirements ### + +* Microsoft Visual C++ 2010 or newer + + + +## Build steps ## + +The AnyRtttl unit test uses the CMake build system to generate a platform-specific build environment. CMake reads the CMakeLists.txt files, checks for installed dependencies and then generates files for the selected build system. + +The following steps show how to build the library: + +1) Download the source code from an existing [tags](https://github.com/end2endzone/AnyRtttl/tags) and extract the content to a local directory (for example `c:\projects\AnyRtttl` or `~/dev/AnyRtttl`). + +2) Open a Command Prompt (Windows) or Terminal (Linux) and browse to the project directory. + +3) Enter the following commands to generate the project files for your build system: +``` +mkdir build +cd build +cmake .. +``` + +4) Build the source code. + +**Windows** +``` +cmake --build . --config Release +``` + +**Linux** +``` +make +``` + + + + +# Testing # + +AnyRtttl comes with unit tests which help maintaining the product stability and level of quality. + +Test are build using the Google Test v1.8.0 framework. For more information on how googletest is working, see the [google test documentation primer](https://github.com/google/googletest/blob/release-1.8.0/googletest/docs/V1_6_Primer.md). + +To run tests, open a shell prompt and browse to the `build/bin` folder and run `anyrtttl_unittest` executable. For Windows users, the executable is located in `build\bin\Release`. + +Test results are saved in junit format in file `anyrtttl_unittest.release.xml`. + +The latest test results are available at the beginning of the [README.md](README.md) file. diff --git a/lib/AnyRtttl/LICENSE b/lib/AnyRtttl/LICENSE new file mode 100644 index 0000000..dd4add2 --- /dev/null +++ b/lib/AnyRtttl/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Antoine Beauchamp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/AnyRtttl/README.md b/lib/AnyRtttl/README.md new file mode 100644 index 0000000..d26d7aa --- /dev/null +++ b/lib/AnyRtttl/README.md @@ -0,0 +1,523 @@ +![AnyRtttl logo](https://github.com/end2endzone/AnyRtttl/raw/master/docs/AnyRtttl-splashscreen.png) + + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Github Releases](https://img.shields.io/github/release/end2endzone/AnyRtttl.svg)](https://github.com/end2endzone/AnyRtttl/releases) + + + +# AnyRtttl # + +AnyRtttl is a feature rich arduino library for playing [RTTTL](http://www.end2endzone.com/anyrtttl-a-feature-rich-arduino-library-for-playing-rtttl-melodies/#Quick_recall_of_the_RTTTL_format) melodies. The library offers much more interesting features than relying on the widely available `void play_rtttl(char *p)` function. The library supports all best RTTTL features. + +Library features: + +* Really small increase in memory & code footprint compared to the usual blocking algorithm. +* Blocking & Non-Blocking modes available. +* Support custom `tone()`, `noTone()`, `delay()` and `millis()` functions. +* Compatible with external Tone libraries. +* Supports highly compressed RTTTL binary format. +* Supports RTTTL melodies stored in Program Memory (`PROGMEM`). +* Play two monolithic melodies on two different pins using 2 piezo buzzer with the help of an external Tone library. + + + +## Status ## + +Build: + +| Service | Build | Tests | +|----|-------|-------| +| AppVeyor | [![Build status](https://img.shields.io/appveyor/ci/end2endzone/AnyRtttl/master.svg?logo=appveyor)](https://ci.appveyor.com/project/end2endzone/AnyRtttl) | [![Tests status](https://img.shields.io/appveyor/tests/end2endzone/AnyRtttl/master.svg?logo=appveyor)](https://ci.appveyor.com/project/end2endzone/AnyRtttl/branch/master/tests) | +| Windows Server 2019 | [![Build on Windows](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_windows.yml/badge.svg)](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_windows.yml) | [![Tests on Windows](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/end2endzone/58cf6c72c08e706335337d5ef9ca48e8/raw/AnyRtttl.master.Windows.json)](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_windows.yml) | +| Ubuntu 22.04 | [![Build on Linux](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_linux.yml/badge.svg)](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_linux.yml) | [![Tests on Linux](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/end2endzone/58cf6c72c08e706335337d5ef9ca48e8/raw/AnyRtttl.master.Linux.json)](https://github.com/end2endzone/AnyRtttl/actions/workflows/build_linux.yml) | + +Statistics: + +| AppVeyor | GitHub | +|--------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| [![Statistics](https://buildstats.info/appveyor/chart/end2endzone/AnyRtttl)](https://ci.appveyor.com/project/end2endzone/AnyRtttl/branch/master) | [![Statistics](https://buildstats.info/github/chart/end2endzone/AnyRtttl)](https://github.com/end2endzone/AnyRtttl/actions) | + + + + +# Purpose # + +After publishing [NonBlockingRtttl](https://github.com/end2endzone/NonBlockingRTTTL) arduino library, I started using the library in more complex projects which was requiring other libraries. I quickly ran into the hell of library dependencies and library conflicts. I realized that I needed more features that could help me prototype faster. + +Other libraries available which allows you to "play" a melody in [RTTTL](http://www.end2endzone.com/anyrtttl-a-feature-rich-arduino-library-for-playing-rtttl-melodies/#Quick_recall_of_the_RTTTL_format) format suffer the same issue: they are based on blocking APIs or the RTTTL data is not optimized for space. + +AnyRtttl is different since it packs multiple RTTTL related features in a single library. It supports [blocking](https://en.wikipedia.org/wiki/Blocking_(computing)) & [non-blocking](http://en.wikipedia.org/wiki/Non-blocking_algorithm) API which makes it suitable to be used by more advanced algorithm. For instance, when using the non-blocking API, the melody can be stopped when a button is pressed. The library is also compatible with external Tone libraries and it supports highly compressed RTTTL binary formats. + + + +## Non-Blocking ## + +Most of the code that can "play" a melody on internet are build the same way: sequential calls to `tone()` and `delay()` functions using hardcoded values. This type of implementation might be good for robots but not for realtime application or projects that needs to monitor pins while the song is playing. + +With AnyRtttl non-blocking mode, your program can read/write IOs pins while playing and react on changes. Implementing a "stop" or "next song" push button is easy! + + + +## External Tone or Timer #0 libraries ## + +The AnyRtttl library is also flexible by allowing you to use the build-in arduino `tone()` and `noTone()` functions or an implementation from any external library which makes it compatible with any Tone library in the market. + +The library also supports custom `delay()` and `millis()` functions. If a project requires modification to the microcontroller's build-in Timer #0, the `millis()` function may be impacted and behave incorrectly. To maximize compatibility, one can supply a custom function which behaves like the original to prevent altering playback. + + + +## Binary RTTTL ## + +The AnyRtttl library also supports playing RTTTL data which is stored as binary data instead of text. This is actually a custom implementation of the RTTTL format. Using this format, one can achieve storing an highly compressed RTTTL melody which saves memory. + +See below for details on RTTTL binary format. + + + + +# Usage # + +The following instructions show how to use the library. + +Define `ANY_RTTTL_INFO` to enable the debugging of the library state on the serial port. + +Use `ANY_RTTTL_VERSION` to get the current version of the library. + +Note, the specified macros must be defined before including `anyrtttl.h` in your sketches. + + + +## Non-blocking mode ## + + anyrtttl::nonblocking::begin(BUZZER_PIN, mario); +Call `anyrtttl::nonblocking::begin()` to setup AnyRtttl library in non-blocking mode. + +Then call `anyrtttl::nonblocking::play()` to update the library's state and play notes as required. + +Use `anyrtttl::done()` or `anyrtttl::nonblocking::isPlaying()` to know if the library is done playing the given song. + +Anytime, one can call `anyrtttl::nonblocking::stop()` to stop playing the current song. + +The following code shows how to use the library in non-blocking mode: + +```cpp +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char * arkanoid = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char * mario = "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; +byte songIndex = 0; //which song to play when the previous one finishes + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + // If we are not playing something + if ( !anyrtttl::nonblocking::isPlaying() ) + { + // Play a song based on songIndex. + if (songIndex == 0) + anyrtttl::nonblocking::begin(BUZZER_PIN, tetris); + else if (songIndex == 1) + anyrtttl::nonblocking::begin(BUZZER_PIN, arkanoid); + else if (songIndex == 2) + anyrtttl::nonblocking::begin(BUZZER_PIN, mario); + + //Set songIndex ready for next song + songIndex++; + } + else + { + anyrtttl::nonblocking::play(); + } +} +``` + + + +## Playing RTTTL data stored in flash (program) memory ## + +AnyRtttl also supports RTTTL melodies stored in flash or Program Memory (PROGMEM). + +The `anyrtttl::nonblocking::begin()` function supports _Program Memory_ macros such as `FPSTR()` or `F()`. + +The following code shows how to use the library with RTTTL data stored in flash (program) memory instead of SRAM: + +```cpp +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char tetris[] PROGMEM = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char arkanoid[] PROGMEM = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char mario[] PROGMEM = "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; +// James Bond theme defined in inline code below (also stored in flash memory) +byte songIndex = 0; //which song to play when the previous one finishes + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + // If we are not playing something + if ( !anyrtttl::nonblocking::isPlaying() ) + { + // Play a song based on songIndex. + if (songIndex == 0) + anyrtttl::nonblocking::beginProgMem(BUZZER_PIN, tetris); + else if (songIndex == 1) + anyrtttl::nonblocking::begin_P(BUZZER_PIN, arkanoid); + else if (songIndex == 2) + anyrtttl::nonblocking::begin(BUZZER_PIN, FPSTR(mario)); + else if (songIndex == 3) + anyrtttl::nonblocking::begin(BUZZER_PIN, F("Bond:d=4,o=5,b=80:32p,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d#6,16d#6,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d6,16c#6,16c#7,c.7,16g#6,16f#6,g#.6")); + + //Set songIndex ready for next song + songIndex++; + } + else + { + anyrtttl::nonblocking::play(); + } +} +``` + + + +# Advanced Usage # + + + +## Custom Tone function (a.k.a. RTTTL 2 code) ## + +This example shows how custom functions can be used by the AnyRtttl library to convert an RTTTL melody to arduino code. + +First define replacement functions like the following: + +```cpp +void serialTone(byte pin, uint16_t frequency, uint32_t duration) { + Serial.print("tone("); + Serial.print(pin); + Serial.print(","); + Serial.print(frequency); + Serial.print(","); + Serial.print(duration); + Serial.println(");"); +} + +void serialNoTone(byte pin) { + Serial.print("noTone("); + Serial.print(pin); + Serial.println(");"); +} + +void serialDelay(uint32_t duration) { + Serial.print("delay("); + Serial.print(duration); + Serial.println(");"); +} +``` + +Each new functions prints the function call & arguments to the serial port. + +In the `setup()` function, setup the AnyRtttl library to use the new functions: + +```cpp +//Use custom functions +anyrtttl::setToneFunction(&serialTone); +anyrtttl::setNoToneFunction(&serialNoTone); +anyrtttl::setDelayFunction(&serialDelay); +``` + +Use the `anyrtttl::blocking::play()` API for "playing" an RTTTL melody and monitor the output of the serial port to see the actual arduino code generated by the library. + +The following code shows how to use the library with custom functions: + +```cpp +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; + +//******************************************************************************************************************* +// The following replacement functions prints the function call & parameters to the serial port. +//******************************************************************************************************************* +void serialTone(byte pin, uint16_t frequency, uint32_t duration) { + Serial.print("tone("); + Serial.print(pin); + Serial.print(","); + Serial.print(frequency); + Serial.print(","); + Serial.print(duration); + Serial.println(");"); +} + +void serialNoTone(byte pin) { + Serial.print("noTone("); + Serial.print(pin); + Serial.println(");"); +} + +void serialDelay(uint32_t duration) { + Serial.print("delay("); + Serial.print(duration); + Serial.println(");"); +} + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); + + //Use custom functions + anyrtttl::setToneFunction(&serialTone); + anyrtttl::setNoToneFunction(&serialNoTone); + anyrtttl::setDelayFunction(&serialDelay); +} + +void loop() { + anyrtttl::blocking::play(BUZZER_PIN, tetris); + + while(true) + { + } +} +``` + + +## Play 16 bits per note RTTTL ## + +Note that this mode requires that an RTTTL melody be already converted to 16-bits per note binary format. + +Use the `anyrtttl::blocking::play16Bits()` API for playing an RTTTL melody encoded as 16 bits per note. + +The following code shows how to use the library with 16-bits per note binary RTTTL: + +```cpp +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 + +//RTTTL 16 bits binary format for the following: tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a +const unsigned char tetris16[] = {0x0A, 0x14, 0x12, 0x02, 0x33, 0x01, 0x03, 0x02, 0x0B, 0x02, 0x14, 0x02, 0x0C, 0x02, 0x03, 0x02, 0x33, 0x01, 0x2A, 0x01, 0x2B, 0x01, 0x03, 0x02, 0x12, 0x02, 0x0B, 0x02, 0x03, 0x02, 0x32, 0x01, 0x33, 0x01, 0x03, 0x02, 0x0A, 0x02, 0x12, 0x02, 0x02, 0x02, 0x2A, 0x01, 0x29, 0x01, 0x3B, 0x01, 0x0A, 0x02, 0x1B, 0x02, 0x2A, 0x02, 0x23, 0x02, 0x1B, 0x02, 0x12, 0x02, 0x13, 0x02, 0x03, 0x02, 0x12, 0x02, 0x0B, 0x02, 0x03, 0x02, 0x32, 0x01, 0x33, 0x01, 0x03, 0x02, 0x0A, 0x02, 0x12, 0x02, 0x02, 0x02, 0x2A, 0x01, 0x2A, 0x01}; +const int tetris16_length = 42; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + anyrtttl::blocking::play16Bits(BUZZER_PIN, tetris16, tetris16_length); + + while(true) + { + } +} +``` + + + +## Play 10 bits per note RTTTL ## + +Note that this mode requires that an RTTTL melody be already converted to 10-bits per note binary format. + +Create a function that will be used by AnyRtttl library to read bits as required. The signature of the library must look like this: `uint16_t function_name(uint8_t numBits)`. + +Note that this code requires the [BitReader](https://github.com/end2endzone/BitReader) library to extract bits from the RTTTL binary buffer. The implementation of `readNextBits()` function delegates the job to the BitReader's `read()` method. + +In the `setup()` function, setup the external library that is used for reading bits: `bitreader.setBuffer(tetris10);`. + +Use the `anyrtttl::blocking::play10Bits()` API for playing an RTTTL melody encoded as 10 bits per note. The 3rd argument of the function requires a pointer to the function extracting bits: `&function_name`. + +The following code shows how to use the library with 10-bits per note binary RTTTL: + +```cpp +#include +#include +#include + +//The BitReader library is required for extracting 10 bit blocks from the RTTTL buffer. +//It can be installed from Arduino Library Manager or from https://github.com/end2endzone/BitReader/releases +#include + +//project's constants +#define BUZZER_PIN 8 + +//RTTTL 10 bits binary format for the following: tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a +const unsigned char tetris10[] = {0x0A, 0x14, 0x12, 0xCE, 0x34, 0xE0, 0x82, 0x14, 0x32, 0x38, 0xE0, 0x4C, 0x2A, 0xAD, 0x34, 0xA0, 0x84, 0x0B, 0x0E, 0x28, 0xD3, 0x4C, 0x03, 0x2A, 0x28, 0xA1, 0x80, 0x2A, 0xA5, 0xB4, 0x93, 0x82, 0x1B, 0xAA, 0x38, 0xE2, 0x86, 0x12, 0x4E, 0x38, 0xA0, 0x84, 0x0B, 0x0E, 0x28, 0xD3, 0x4C, 0x03, 0x2A, 0x28, 0xA1, 0x80, 0x2A, 0xA9, 0x04}; +const int tetris10_length = 42; + +//bit reader support +#ifndef USE_BITADDRESS_READ_WRITE +BitReader bitreader; +#else +BitAddress bitreader; +#endif +uint16_t readNextBits(uint8_t numBits) +{ + uint16_t bits = 0; + bitreader.read(numBits, &bits); + return bits; +} + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + bitreader.setBuffer(tetris10); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + anyrtttl::blocking::play10Bits(BUZZER_PIN, tetris10_length, &readNextBits); + + while(true) + { + } +} +``` + + + +## Other ## + +More AnyRtttl examples are also available: + +* [Basic](examples/Basic/Basic.ino) +* [BlockingProgramMemoryRtttl](examples/BlockingProgramMemoryRtttl/BlockingProgramMemoryRtttl.ino) +* [BlockingRtttl](examples/BlockingRtttl/BlockingRtttl.ino) +* [BlockingWithNonBlocking](examples/BlockingWithNonBlocking/BlockingWithNonBlocking.ino) +* [NonBlockingProgramMemoryRtttl](examples/NonBlockingProgramMemoryRtttl/NonBlockingProgramMemoryRtttl.ino) +* [NonBlockingRtttl](examples/NonBlockingRtttl/NonBlockingRtttl.ino) +* [NonBlockingStopBeforeEnd](examples/NonBlockingStopBeforeEnd/NonBlockingStopBeforeEnd.ino) +* [Play10Bits](examples/Play10Bits/Play10Bits.ino) +* [Play16Bits](examples/Play16Bits/Play16Bits.ino) +* [Rtttl2Code](examples/Rtttl2Code/Rtttl2Code.ino) + + + + +# Binary RTTTL format definition # + +The following section defines the field order and size (in bits) required for encoding / decoding of each melody as binary RTTTL. + +This is actually a custom implementation of the RTTTL format. Using this format, one can achieve storing an highly compressed RTTTL melody which saves memory. + +Note that all fields definition are defined in LSB to MSB order. + +## Header ## + +The first 16 bits stores the RTTTL default section (a.k.a header) which is defined as the following: + +| Field name | Size (bits) | Range | Description | +|-------------------------|:-----------:|----------|-------------------------------------------------------------| +| Default duration index | 3 | [0, 7] | Matches the index used for `getNoteDurationFromIndex()` API | +| Default octave index | 2 | [0, 3] | Matches the index used for `getNoteOctaveFromIndex()` API. | +| Beats per minutes (BPM) | 10 | [1, 900] | | +| Padding | 1 | | | + +## Notes ## + +Next is each note's of the melody. Each note is encoded as 10 bits (or 16 bits) per note. Notes are defined as the following: + +| Field name | Size (bits) | Range | Description | +|--------------------|:-----------:|---------|--------------------------------------------------------------| +| Duration index | 3 | [0, 7] | Matches the index used for `getNoteDurationFromIndex()` API. | +| Note letter index | 3 | [0, 7] | Matches the index used for `getNoteLetterFromIndex()` API. | +| Pound | 1 | boolean | Defines if the note is pounded or not. | +| Dotted | 1 | boolean | Defines if the note is dotted or not. | +| Octave index | 2 | [0, 3] | Matches the index used for `getNoteOctaveFromIndex()` API. | +| Padding (optional) | 6 | | See description below. | + +The last field of a note (defined as `Padding`) is an optional 6 bits field. The AnyRtttl library supports both 10 bits per note and 16 bits per note definitions. Use the appropriate API for playing both format. + + + +## 10 bits per note (no padding) ## + +Each RTTTL note is encoded into 10 bits which is the minimum size of a note. This storage method is the best compression method for storing RTTTL melodies and reduces the usage of the dynamic memory to the minimum. + +However, since all notes are not aligned on multiple of 8 bits, addressing each note by an offset is impossible which makes the playback harder. Each notes must be deserialized one after the other from a buffer using blocks of 10 bits which increases the program storage space footprint. + +An external arduino library (or custom code) is required to allow AnyRtttl library to consume bits as needed. The arduino [BitReader](https://github.com/end2endzone/BitReader) library may be used for handling bit deserialization but any library that can extract a given number of bits from a buffer would work. + + + +## 16 bits per note (with padding) ## + +Each RTTTL note is encoded into 16 bits which is much better than the average 3.28 bytes per note text format. This storage method is optimum for storing RTTTL melodies and reduces the usage of the dynamic memory without increasing to much program storage space. + +All notes are aligned on 16 bits. Addressing each note by an offset allows an easy playback. Only the first 10 bits of each 16 bits block is used. The value of the padding field is undefined. + + + +## Playback ## + +The following AnyRtttl blocking APIs are available for playing both binary formats: +* 10 bits per note: `anyrtttl::blocking::play10Bits()`. +* 16 bits per note: `anyrtttl::blocking::play16Bits()`. + + + + +# Building # + +Please refer to file [INSTALL.md](INSTALL.md) for details on how installing/building the application. + + + + +# Platforms # + +AnyRtttl has been tested with the following platform: + + * Linux x86/x64 + * Windows x86/x64 + + + + +# Versioning # + +We use [Semantic Versioning 2.0.0](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/end2endzone/AnyRtttl/tags). + + + + +# Authors # + +* **Antoine Beauchamp** - *Initial work* - [end2endzone](https://github.com/end2endzone) + +See also the list of [contributors](https://github.com/end2endzone/AnyRtttl/blob/master/AUTHORS) who participated in this project. + + + + +# License # + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details diff --git a/lib/AnyRtttl/appveyor.yml b/lib/AnyRtttl/appveyor.yml new file mode 100644 index 0000000..88c6ab9 --- /dev/null +++ b/lib/AnyRtttl/appveyor.yml @@ -0,0 +1,81 @@ +#---------------------------------# +# general configuration # +#---------------------------------# + +# version format +version: "{branch} (#{build})" + +# branches to build +branches: + only: + - master + - /feature-issue.*/ + +#---------------------------------# +# environment configuration # +#---------------------------------# + +# Build worker image (VM template) +image: Visual Studio 2019 + +# scripts that are called at very beginning, before repo cloning +init: + - cmd: git config --global core.autocrlf true + - ps: $env:GIT_HASH=$env:APPVEYOR_REPO_COMMIT.Substring(0, 10) + +# clone directory +clone_folder: c:\projects\AnyRtttl + +# scripts that run after cloning repository +install: +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\install_arduinocli.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_install_libraries.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\install_this.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\install_googletest.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\install_rapidassist.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\install_win32arduino.bat + +#---------------------------------# +# build configuration # +#---------------------------------# + +environment: + PlatformToolset: v142 + +# build platform, i.e. x86, x64, Any CPU. This setting is optional. +platform: Win32 + +# build Configuration, i.e. Debug, Release, etc. +configuration: Release + +build_script: +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\build_library.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\external\BitReader\ci\appveyor\arduino_library_install.bat +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat Basic +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat BlockingProgramMemoryRtttl +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat BlockingRtttl +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat BlockingWithNonBlocking +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat NonBlockingProgramMemoryRtttl +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat NonBlockingRtttl +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat NonBlockingStopBeforeEnd +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat Play10Bits +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat Play16Bits +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\arduino_build_sketch.bat Rtttl2Code + + +#---------------------------------# +# tests configuration # +#---------------------------------# + +# to run your custom scripts instead of automatic tests +test_script: +- cmd: call %APPVEYOR_BUILD_FOLDER%\ci\appveyor\test_script.bat +- ps: . "$env:APPVEYOR_BUILD_FOLDER\ci\appveyor\UploadJUnitFiles.ps1" -Path "$env:APPVEYOR_BUILD_FOLDER\build\bin" + +#---------------------------------# +# artifacts configuration # +#---------------------------------# + +artifacts: +- path: build\bin\Release\anyrtttl_unittest.release.xml + name: anyrtttl_unittest.release.xml diff --git a/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.bat b/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.bat new file mode 100644 index 0000000..5b6f14b --- /dev/null +++ b/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.bat @@ -0,0 +1,4 @@ +@echo off +cd c:\temp +powershell -nologo -executionpolicy bypass -File "%~dpn0.ps1" +pause diff --git a/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.ps1 b/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.ps1 new file mode 100644 index 0000000..5279e82 --- /dev/null +++ b/lib/AnyRtttl/ci/appveyor/UploadJUnitFiles.ps1 @@ -0,0 +1,39 @@ +Param( + [Parameter(Mandatory=$True)] + [string]$Path +) + +function IsJUnitFile([string]$Path) +{ + $JUnitSignature = "NUL 1>NUL +xcopy /s /y *.* %outdir% + +::Zip content +del %outdir%.zip >nul 2>nul +"C:\Program Files\7-Zip\7z" a %outdir%.zip %outdir% -xr!build -xr!third_parties + +::Open an explorer windows with the generated file selected +start "" explorer.exe /select,"%outdir%.zip" + +endlocal diff --git a/lib/AnyRtttl/ci/generic/patch.py b/lib/AnyRtttl/ci/generic/patch.py new file mode 100644 index 0000000..82ebcd6 --- /dev/null +++ b/lib/AnyRtttl/ci/generic/patch.py @@ -0,0 +1,70 @@ +#!/usr/bin/python + +import sys +import getopt +import argparse +import os +import os.path + +def check_file_exists(file_path) : + check_file = os.path.exists(file_path) + if not check_file: + print('File not found: ', file_path) + sys.exit(2) + +def read_file(file_path) : + content = '' + try: + with open(file_path) as f: + content=f.read() + except IOError: + print("Failed reading file: ", file_path) + sys.exit(3) + return content + +def main(argv): + argParser = argparse.ArgumentParser() + argParser.add_argument("-i", "--ifile", help="Input file") + argParser.add_argument("-o", "--ofile", help="Output file") + argParser.add_argument("-p", "--pfile", help="Pattern file") + argParser.add_argument("-r", "--rfile", help="Replace file") + #argParser.add_argument("-i", "--int", type=int, help="your numeric age ") + + args = argParser.parse_args() + #print("args=%s" % args) + + print("Patching file: ", args.ifile) + print("as output file: ", args.ofile) + + check_file_exists(args.ifile) + check_file_exists(args.ofile) + check_file_exists(args.pfile) + check_file_exists(args.rfile) + + # Read search and replace patterns + old_text = read_file(args.pfile) + new_text = read_file(args.rfile) + # print("Pattern: ", old_text) + # print("Replace: ", new_text) + + # Read input file + old_content = read_file(args.ifile) + + # Do the actual search and replace + new_content = old_content.replace(old_text, new_text) + + # Write output file + try: + with open(args.ofile, "w") as f: + f.write(new_content) + except IOError: + print("Failed writing file: ", args.ofile) + sys.exit(3) + + if old_content == new_content: + print("Warning! The given pattern was not found in input file.") + else: + print("success!") + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/lib/AnyRtttl/ci/generic/win32arduino.pattern.txt b/lib/AnyRtttl/ci/generic/win32arduino.pattern.txt new file mode 100644 index 0000000..c11ec6e --- /dev/null +++ b/lib/AnyRtttl/ci/generic/win32arduino.pattern.txt @@ -0,0 +1,2 @@ +#define F(expr) expr +#define PROGMEM \ No newline at end of file diff --git a/lib/AnyRtttl/ci/generic/win32arduino.replace.txt b/lib/AnyRtttl/ci/generic/win32arduino.replace.txt new file mode 100644 index 0000000..2822b49 --- /dev/null +++ b/lib/AnyRtttl/ci/generic/win32arduino.replace.txt @@ -0,0 +1,10 @@ +typedef struct __FlashStringHelper { + char c; +} __FlashStringHelper_t; +#ifndef F +# define F(expr) ((const __FlashStringHelper *)expr) +#endif +#ifndef FPSTR +# define FPSTR(expr) ((const __FlashStringHelper *)expr) +#endif +#define PROGMEM \ No newline at end of file diff --git a/lib/AnyRtttl/ci/github/arduino_build_sketch.bat b/lib/AnyRtttl/ci/github/arduino_build_sketch.bat new file mode 100644 index 0000000..46cb7a5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/arduino_build_sketch.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" "%~1" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/arduino_build_sketch.sh b/lib/AnyRtttl/ci/github/arduino_build_sketch.sh new file mode 100644 index 0000000..c9695af --- /dev/null +++ b/lib/AnyRtttl/ci/github/arduino_build_sketch.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename "$1" diff --git a/lib/AnyRtttl/ci/github/arduino_install_libraries.bat b/lib/AnyRtttl/ci/github/arduino_install_libraries.bat new file mode 100644 index 0000000..46cb7a5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/arduino_install_libraries.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" "%~1" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/arduino_install_libraries.sh b/lib/AnyRtttl/ci/github/arduino_install_libraries.sh new file mode 100644 index 0000000..c9695af --- /dev/null +++ b/lib/AnyRtttl/ci/github/arduino_install_libraries.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename "$1" diff --git a/lib/AnyRtttl/ci/github/build_library.bat b/lib/AnyRtttl/ci/github/build_library.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/build_library.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/build_library.sh b/lib/AnyRtttl/ci/github/build_library.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/build_library.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/install_arduinocli.bat b/lib/AnyRtttl/ci/github/install_arduinocli.bat new file mode 100644 index 0000000..539218c --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_arduinocli.bat @@ -0,0 +1,15 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% + +:: Remember installation directory +echo Remember ARDUINO_CLI_INSTALL_DIR as %ARDUINO_CLI_INSTALL_DIR% in %GITHUB_ENV% +echo ARDUINO_CLI_INSTALL_DIR=%ARDUINO_CLI_INSTALL_DIR%>> %GITHUB_ENV% diff --git a/lib/AnyRtttl/ci/github/install_arduinocli.sh b/lib/AnyRtttl/ci/github/install_arduinocli.sh new file mode 100644 index 0000000..690c37c --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_arduinocli.sh @@ -0,0 +1,17 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +# and execute the script under the current shell instead of loading another one +this_filename=`basename "$0"` +. $GITHUB_WORKSPACE/ci/linux/$this_filename + +# Remember installation directory +echo Remember ARDUINO_CLI_INSTALL_DIR as $ARDUINO_CLI_INSTALL_DIR in $GITHUB_ENV +echo ARDUINO_CLI_INSTALL_DIR=$ARDUINO_CLI_INSTALL_DIR>> $GITHUB_ENV diff --git a/lib/AnyRtttl/ci/github/install_googletest.bat b/lib/AnyRtttl/ci/github/install_googletest.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_googletest.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/install_googletest.sh b/lib/AnyRtttl/ci/github/install_googletest.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_googletest.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/install_rapidassist.bat b/lib/AnyRtttl/ci/github/install_rapidassist.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_rapidassist.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/install_rapidassist.sh b/lib/AnyRtttl/ci/github/install_rapidassist.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_rapidassist.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/install_this.bat b/lib/AnyRtttl/ci/github/install_this.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_this.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/install_this.sh b/lib/AnyRtttl/ci/github/install_this.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_this.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/install_win32arduino.bat b/lib/AnyRtttl/ci/github/install_win32arduino.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_win32arduino.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/install_win32arduino.sh b/lib/AnyRtttl/ci/github/install_win32arduino.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/install_win32arduino.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/maketestbadge.py b/lib/AnyRtttl/ci/github/maketestbadge.py new file mode 100644 index 0000000..968bef4 --- /dev/null +++ b/lib/AnyRtttl/ci/github/maketestbadge.py @@ -0,0 +1,165 @@ +#!/usr/bin/python + +import sys + +import os +from os import path + +import io +import itertools as IT +import xml.etree.ElementTree as ET + +import platform + +LEVEL_SUCCESS = 0 +LEVEL_WARNING = 1 +LEVEL_ERROR = 2 + +# Queries examples: +# https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fend2endzone%2Ffa6f6ec2f8315e357405164b4d618ef4%2Fraw%2Fafe1c3be748e7a78bc905c322afbd9a24f1e328e%2Fmybadge.json +# https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fend2endzone%2Ffa6f6ec2f8315e357405164b4d618ef4%2Fraw%2F5df297ffc360897097974338d4d43ef69271831b%2Fmybadge.json +# https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Fend2endzone%2Ffa6f6ec2f8315e357405164b4d618ef4%2Fraw%2Fmybadge.json + +def getNamedLogo(): + # Search the running CI/CD service from environment variables + namedLogo = "" + if os.getenv('APPVEYOR', "") != "": + namedLogo = "AppVeyor" + elif os.getenv('TRAVIS', "") != "": + namedLogo = "Travis CI" + elif os.getenv('JENKINS_URL', "") != "": + namedLogo = "Jenkins" + elif os.getenv('GITHUB_ACTIONS', "") != "": + if os.getenv('RUNNER_OS', "") == "macOS": + namedLogo = "Apple" + elif os.getenv('RUNNER_OS', "") == "Linux": + namedLogo = "Linux" + elif os.getenv('RUNNER_OS', "") == "Windows": + namedLogo = "Windows" + else: + namedLogo = "GitHub" + elif platform.system() == "Darwin": + namedLogo = "Apple" + elif platform.system() == "Linux": + namedLogo = "Linux" + elif platform.system() == "Windows": + namedLogo = "Windows" + return namedLogo + +def getColorFromLevel(level): + color = "" + if level == LEVEL_SUCCESS: + color = "Green" + elif level == LEVEL_WARNING: + color = "Orange" + else: + color = "Red" + return color + +def main(): + print("maketestbadge v1.1") + + # Validate input file + if len(sys.argv) == 2: + file_path = sys.argv[1] + else: + print("Create endpoint badge json files from junit report. See https://shields.io/endpoint for details.") + print("Missing input file. Please specify a path to a junit report.") + sys.exit(1); + if not path.isfile(file_path) or not path.exists(file_path): + print("File not found: " + file_path) + sys.exit(1); + + print("Creating badge from junit report '" + file_path + "' for https://shields.io/endpoint") + + # Parse the content of the file + try: + tree = ET.parse(file_path) + except ET.ParseError as err: + lineno, column = err.position + err.msg = "Failed parsing file at line=" + str(lineno) + ", column=" + str(column) + "." + #raise + print(err.msg) + sys.exit(1); + root = tree.getroot() + + # Find the important attributes + #testsuites = root.find('./testsuite') + try: + tests_count = root.attrib['tests'] + failures_count = root.attrib['failures'] + disabled_count = root.attrib['disabled'] + except KeyError as err: + err.msg = "Failed to find count of tests, failures or disabled values." + #raise + print(err.msg) + sys.exit(1); + tests_count = int(tests_count ) + failures_count = int(failures_count) + disabled_count = int(disabled_count) + success_count = tests_count - failures_count - disabled_count + print("Found " + str(tests_count) + " tests: " + str(success_count) + " success, " + str(failures_count) + " failures and " + str(disabled_count) + " disabled tests.") + + # Evaluate badge properties + # badge_level + if failures_count == 0: + badge_level = LEVEL_SUCCESS + elif failures_count == 1: + badge_level = LEVEL_WARNING + else: + badge_level = LEVEL_ERROR + + # badge_color, a.k.a message color + if badge_level == LEVEL_SUCCESS: + badge_color = "#4c1" #brightgreen + elif badge_level == LEVEL_WARNING: + badge_color = "#fe7d37" #orange + else: + badge_color = "#e05d44" #red + + # badge_message + if badge_level == LEVEL_SUCCESS: + badge_message = str(success_count) + " passed" + elif badge_level == LEVEL_WARNING: + badge_message = str(failures_count) + " failed" + else: + badge_message = str(failures_count) + " failed" + #if disabled_count > 0: + # badge_message = badge_message + ", " + str(disabled_count) + " disabled" + + # other + badge_schemaVersion = 1 + badge_label = "tests" + #badge_labelColor = "#5c5c5c" + badge_namedLogo = getNamedLogo() + badge_logoColor = "white" + + print("Creating badge: " + badge_namedLogo + ", " + getColorFromLevel(badge_level).lower() + ", " + badge_message) + + relative_path = "badge.json" + full_path = os.path.realpath(relative_path) + + # Save as badge.json + try: + text_file = open(full_path, "w") + text_file.write("{\n") + text_file.write(" \"schemaVersion\": {0},\n".format(badge_schemaVersion)) + text_file.write(" \"namedLogo\": \"{0}\",\n".format(badge_namedLogo)) + text_file.write(" \"logoColor\": \"{0}\",\n".format(badge_logoColor)) + text_file.write(" \"label\": \"{0}\",\n".format(badge_label)) + #text_file.write(" \"labelColor\": \"{0}\",\n".format(badge_labelColor)) + text_file.write(" \"color\": \"{0}\",\n".format(badge_color)) + text_file.write(" \"message\": \"{0}\"\n".format(badge_message)) + text_file.write("}\n") + text_file.close() + except OSError as err: + err.msg = "Failed to save badge.json." + #raise + print(err.msg) + sys.exit(1); + + print("Saved badge as " + full_path) + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/lib/AnyRtttl/ci/github/test_script.bat b/lib/AnyRtttl/ci/github/test_script.bat new file mode 100644 index 0000000..3879bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/github/test_script.bat @@ -0,0 +1,11 @@ +@echo off + +:: Validate GitHub CI's environment +if "%GITHUB_WORKSPACE%"=="" ( + echo Please define 'GITHUB_WORKSPACE' environment variable. + exit /B 1 +) + +:: Call matching script for windows +call "%GITHUB_WORKSPACE%\ci\windows\%~n0.bat" +if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/lib/AnyRtttl/ci/github/test_script.sh b/lib/AnyRtttl/ci/github/test_script.sh new file mode 100644 index 0000000..23af3c6 --- /dev/null +++ b/lib/AnyRtttl/ci/github/test_script.sh @@ -0,0 +1,12 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate GitHub CI environment +if [ "$GITHUB_WORKSPACE" = "" ]; then + echo "Please define 'GITHUB_WORKSPACE' environment variable."; + exit 1; +fi + +# Call matching script for linux +this_filename=`basename "$0"` +$GITHUB_WORKSPACE/ci/linux/$this_filename diff --git a/lib/AnyRtttl/ci/github/tests_not_available.badge.json b/lib/AnyRtttl/ci/github/tests_not_available.badge.json new file mode 100644 index 0000000..d316982 --- /dev/null +++ b/lib/AnyRtttl/ci/github/tests_not_available.badge.json @@ -0,0 +1,8 @@ +{ + "schemaVersion": 1, + "namedLogo": "GitHub", + "logoColor": "white", + "label": "tests", + "color": "inactive", + "message": "not available" +} diff --git a/lib/AnyRtttl/ci/linux/arduino_build_sketch.sh b/lib/AnyRtttl/ci/linux/arduino_build_sketch.sh new file mode 100644 index 0000000..9316b21 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/arduino_build_sketch.sh @@ -0,0 +1,30 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Check Arduino CLI installation +echo Expecting Arduino IDE installed in directory: $ARDUINO_CLI_INSTALL_DIR +echo Searching for arduino cli executable... +export PATH=$PATH:$ARDUINO_CLI_INSTALL_DIR +which arduino-cli +echo + +export ARDUINO_INO_FILE=$PRODUCT_SOURCE_DIR/examples/$1/$1.ino + +echo ========================================================================================================== +echo Compiling $ARDUINO_INO_FILE +echo ========================================================================================================== +cd $PRODUCT_SOURCE_DIR/examples/$1 +arduino-cli compile -b arduino:avr:nano:cpu=atmega328 $1.ino + +cd "$(dirname "$0")" diff --git a/lib/AnyRtttl/ci/linux/arduino_install_libraries.sh b/lib/AnyRtttl/ci/linux/arduino_install_libraries.sh new file mode 100644 index 0000000..f8d6d05 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/arduino_install_libraries.sh @@ -0,0 +1,30 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Check Arduino CLI installation +echo Expecting Arduino IDE installed in directory: $ARDUINO_CLI_INSTALL_DIR +echo Searching for arduino cli executable... +export PATH=$PATH:$ARDUINO_CLI_INSTALL_DIR +which arduino-cli +echo + +echo ========================================================================================================== +echo Installing arduino library dependencies +echo ========================================================================================================== + +echo BitReader... +arduino-cli lib install BitReader +echo + +cd "$(dirname "$0")" diff --git a/lib/AnyRtttl/ci/linux/build_library.sh b/lib/AnyRtttl/ci/linux/build_library.sh new file mode 100644 index 0000000..b33a024 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/build_library.sh @@ -0,0 +1,38 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate mandatory environment variables +if [ "$PRODUCT_BUILD_TYPE" = "" ]; then + echo "Please define 'PRODUCT_BUILD_TYPE' environment variable."; + exit 1; +fi + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +unset CMAKE_PREFIX_PATH +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/googletest/install" +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/RapidAssist/install" +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/win32Arduino/install" + +echo ============================================================================ +echo Generating AnyRtttl... +echo ============================================================================ +cd $PRODUCT_SOURCE_DIR +mkdir -p build +cd build +cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" -DANYRTTTL_BUILD_EXAMPLES=ON .. + +echo ============================================================================ +echo Compiling AnyRtttl... +echo ============================================================================ +cmake --build . +echo diff --git a/lib/AnyRtttl/ci/linux/install_arduinocli.sh b/lib/AnyRtttl/ci/linux/install_arduinocli.sh new file mode 100644 index 0000000..eea4d15 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/install_arduinocli.sh @@ -0,0 +1,33 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Set download filename +export ARDUINO_CLI_FILENAME="" +if [ "$RUNNER_OS" = "Linux" ]; then + export ARDUINO_CLI_FILENAME=arduino-cli_latest_Linux_64bit.tar.gz +elif [ "$RUNNER_OS" = "macOS" ]; then + export ARDUINO_CLI_FILENAME=arduino-cli_latest_macOS_64bit.tar.gz +fi + +# Download +echo Downloading file https://downloads.arduino.cc/arduino-cli/$ARDUINO_CLI_FILENAME +wget --no-verbose https://downloads.arduino.cc/arduino-cli/$ARDUINO_CLI_FILENAME +echo + +# Installing +export ARDUINO_CLI_INSTALL_DIR=$HOME/arduino-cli +echo Installing Arduino CLI to directory: $ARDUINO_CLI_INSTALL_DIR +mkdir -p $ARDUINO_CLI_INSTALL_DIR +tar xf $ARDUINO_CLI_FILENAME --directory $ARDUINO_CLI_INSTALL_DIR +echo + +# Verify +echo Searching for arduino cli executable... +export PATH=$PATH:$ARDUINO_CLI_INSTALL_DIR +which arduino-cli +arduino-cli version +echo + +echo Installing arduino:avr core... +arduino-cli core install arduino:avr +echo diff --git a/lib/AnyRtttl/ci/linux/install_googletest.sh b/lib/AnyRtttl/ci/linux/install_googletest.sh new file mode 100644 index 0000000..317c0b2 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/install_googletest.sh @@ -0,0 +1,56 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate mandatory environment variables +if [ "$PRODUCT_BUILD_TYPE" = "" ]; then + echo "Please define 'PRODUCT_BUILD_TYPE' environment variable."; + exit 1; +fi + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Prepare CMAKE parameters +export CMAKE_INSTALL_PREFIX="$PRODUCT_SOURCE_DIR/third_parties/googletest/install" +unset CMAKE_PREFIX_PATH +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;" + +echo ============================================================================ +echo Cloning googletest into $PRODUCT_SOURCE_DIR/third_parties/googletest +echo ============================================================================ +mkdir -p "$PRODUCT_SOURCE_DIR/third_parties" +cd "$PRODUCT_SOURCE_DIR/third_parties" +git clone "https://github.com/google/googletest.git" +cd googletest +echo + +echo Checking out version 1.8.0... +git -c advice.detachedHead=false checkout release-1.8.0 +echo + +echo ============================================================================ +echo Generating googletest... +echo ============================================================================ +mkdir -p build +cd build +cmake -Wno-dev -DCMAKE_BUILD_TYPE=$PRODUCT_BUILD_TYPE -DBUILD_SHARED_LIBS=OFF -DBUILD_GMOCK=OFF -DBUILD_GTEST=ON -DCMAKE_INSTALL_PREFIX="$CMAKE_INSTALL_PREFIX" -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" .. + +echo ============================================================================ +echo Compiling googletest... +echo ============================================================================ +cmake --build . -- -j4 +echo + +echo ============================================================================ +echo Installing googletest into $CMAKE_INSTALL_PREFIX +echo ============================================================================ +make install +echo diff --git a/lib/AnyRtttl/ci/linux/install_rapidassist.sh b/lib/AnyRtttl/ci/linux/install_rapidassist.sh new file mode 100644 index 0000000..3221bc5 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/install_rapidassist.sh @@ -0,0 +1,56 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate mandatory environment variables +if [ "$PRODUCT_BUILD_TYPE" = "" ]; then + echo "Please define 'PRODUCT_BUILD_TYPE' environment variable."; + exit 1; +fi + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Prepare CMAKE parameters +export CMAKE_INSTALL_PREFIX="$PRODUCT_SOURCE_DIR/third_parties/RapidAssist/install" +unset CMAKE_PREFIX_PATH +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/googletest/install" + +echo ============================================================================ +echo Cloning RapidAssist into $PRODUCT_SOURCE_DIR/third_parties/RapidAssist +echo ============================================================================ +mkdir -p "$PRODUCT_SOURCE_DIR/third_parties" +cd "$PRODUCT_SOURCE_DIR/third_parties" +git clone "https://github.com/end2endzone/RapidAssist.git" +cd RapidAssist +echo + +echo Checking out version v0.10.2... +git -c advice.detachedHead=false checkout 0.10.2 +echo + +echo ============================================================================ +echo Generating RapidAssist... +echo ============================================================================ +mkdir -p build +cd build +cmake -Wno-dev -DCMAKE_BUILD_TYPE=$PRODUCT_BUILD_TYPE -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX="$CMAKE_INSTALL_PREFIX" -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" .. + +echo ============================================================================ +echo Compiling RapidAssist... +echo ============================================================================ +cmake --build . -- -j4 +echo + +echo ============================================================================ +echo Installing RapidAssist into $CMAKE_INSTALL_PREFIX +echo ============================================================================ +make install +echo diff --git a/lib/AnyRtttl/ci/linux/install_this.sh b/lib/AnyRtttl/ci/linux/install_this.sh new file mode 100644 index 0000000..95e8b91 --- /dev/null +++ b/lib/AnyRtttl/ci/linux/install_this.sh @@ -0,0 +1,19 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Create libraries folder for current user +mkdir -p $HOME/Arduino/libraries + +# Install current library to Arduino Library repository +ln -s $PRODUCT_SOURCE_DIR $HOME/Arduino/libraries/. diff --git a/lib/AnyRtttl/ci/linux/install_win32arduino.sh b/lib/AnyRtttl/ci/linux/install_win32arduino.sh new file mode 100644 index 0000000..51cd34e --- /dev/null +++ b/lib/AnyRtttl/ci/linux/install_win32arduino.sh @@ -0,0 +1,64 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate mandatory environment variables +if [ "$PRODUCT_BUILD_TYPE" = "" ]; then + echo "Please define 'PRODUCT_BUILD_TYPE' environment variable."; + exit 1; +fi + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +# Prepare CMAKE parameters +export CMAKE_INSTALL_PREFIX="$PRODUCT_SOURCE_DIR/third_parties/win32Arduino/install" +unset CMAKE_PREFIX_PATH +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/googletest/install" +export CMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH;$PRODUCT_SOURCE_DIR/third_parties/RapidAssist/install" + +echo ============================================================================ +echo Cloning win32Arduino into $PRODUCT_SOURCE_DIR/third_parties/win32Arduino +echo ============================================================================ +mkdir -p "$PRODUCT_SOURCE_DIR/third_parties" +cd "$PRODUCT_SOURCE_DIR/third_parties" +git clone "https://github.com/end2endzone/win32Arduino.git" +cd win32Arduino +echo + +echo Checking out version v2.4.0... +git -c advice.detachedHead=false checkout 2.4.0 +echo + +echo ============================================================================ +echo Patching win32Arduino for type __FlashStringHelper... +echo ============================================================================ +python "$PRODUCT_SOURCE_DIR/ci/generic/patch.py" -i "$PRODUCT_SOURCE_DIR/third_parties/win32Arduino/include/avr/pgmspace.h" -o "$PRODUCT_SOURCE_DIR/third_parties/win32Arduino/include/avr/pgmspace.h" -p "$PRODUCT_SOURCE_DIR/ci/generic/win32arduino.pattern.txt" -r "$PRODUCT_SOURCE_DIR/ci/generic/win32arduino.replace.txt" +echo + +echo ============================================================================ +echo Generating win32Arduino... +echo ============================================================================ +mkdir -p build +cd build +cmake -Wno-dev -DCMAKE_BUILD_TYPE=$PRODUCT_BUILD_TYPE -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX="$CMAKE_INSTALL_PREFIX" -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" .. +echo + +echo ============================================================================ +echo Compiling win32Arduino... +echo ============================================================================ +cmake --build . -- -j4 +echo + +echo ============================================================================ +echo Installing win32Arduino into $CMAKE_INSTALL_PREFIX +echo ============================================================================ +make install +echo diff --git a/lib/AnyRtttl/ci/linux/test_script.sh b/lib/AnyRtttl/ci/linux/test_script.sh new file mode 100644 index 0000000..efc793a --- /dev/null +++ b/lib/AnyRtttl/ci/linux/test_script.sh @@ -0,0 +1,33 @@ +# Any commands which fail will cause the shell script to exit immediately +set -e + +# Validate mandatory environment variables +if [ "$PRODUCT_BUILD_TYPE" = "" ]; then + echo "Please define 'PRODUCT_BUILD_TYPE' environment variable."; + exit 1; +fi + +# Set PRODUCT_SOURCE_DIR root directory +if [ "$PRODUCT_SOURCE_DIR" = "" ]; then + RESTORE_DIRECTORY="$PWD" + cd "$(dirname "$0")" + cd ../.. + export PRODUCT_SOURCE_DIR="$PWD" + echo "PRODUCT_SOURCE_DIR set to '$PRODUCT_SOURCE_DIR'." + cd "$RESTORE_DIRECTORY" + unset RESTORE_DIRECTORY +fi + +echo ============================================================================ +echo Running unit tests... +echo ============================================================================ +cd "$PRODUCT_SOURCE_DIR/build/bin" +if [ "$PRODUCT_BUILD_TYPE" = "Debug" ]; then + ./anyrtttl_unittest-d || true; #do not fail build even if a test fails. +else + ./anyrtttl_unittest || true; #do not fail build even if a test fails. +fi + +# Note: +# GitHub Action do not support uploading test results in a nice GUI. There is no build-in way to detect a failed test. +# Do not reset the error returned by unit test execution. This will actually fail the build and will indicate in GitHub that a test has failed. diff --git a/lib/AnyRtttl/ci/windows/arduino_build_sketch.bat b/lib/AnyRtttl/ci/windows/arduino_build_sketch.bat new file mode 100644 index 0000000..bcbc2fd --- /dev/null +++ b/lib/AnyRtttl/ci/windows/arduino_build_sketch.bat @@ -0,0 +1,33 @@ +@echo off + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Check Arduino CLI installation +echo Expecting Arduino CLI installed in directory: %ARDUINO_CLI_INSTALL_DIR% +echo Searching for arduino cli executable... +set PATH=%PATH%;%ARDUINO_CLI_INSTALL_DIR% +where arduino-cli.exe +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +set ARDUINO_INO_FILE=%PRODUCT_SOURCE_DIR%\examples\%~1\%~1.ino + +echo ========================================================================================================== +echo Compiling %ARDUINO_INO_FILE% +echo ========================================================================================================== +cd /d "%PRODUCT_SOURCE_DIR%\examples\%~1" +arduino-cli compile -b arduino:avr:nano:cpu=atmega328 %~1.ino +if %errorlevel% neq 0 exit /b %errorlevel% + +cd /d "%~dp0" diff --git a/lib/AnyRtttl/ci/windows/arduino_install_libraries.bat b/lib/AnyRtttl/ci/windows/arduino_install_libraries.bat new file mode 100644 index 0000000..232efc9 --- /dev/null +++ b/lib/AnyRtttl/ci/windows/arduino_install_libraries.bat @@ -0,0 +1,33 @@ +@echo off + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Check Arduino CLI installation +echo Expecting Arduino CLI installed in directory: %ARDUINO_CLI_INSTALL_DIR% +echo Searching for arduino cli executable... +set PATH=%PATH%;%ARDUINO_CLI_INSTALL_DIR% +where arduino-cli.exe +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ========================================================================================================== +echo Installing arduino library dependencies +echo ========================================================================================================== + +echo BitReader +arduino-cli lib install BitReader +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +cd /d "%~dp0" diff --git a/lib/AnyRtttl/ci/windows/build_library.bat b/lib/AnyRtttl/ci/windows/build_library.bat new file mode 100644 index 0000000..a3cda5d --- /dev/null +++ b/lib/AnyRtttl/ci/windows/build_library.bat @@ -0,0 +1,48 @@ +@echo off + +:: Validate mandatory environment variables +if "%CONFIGURATION%"=="" ( + echo Please define 'Configuration' environment variable. + exit /B 1 +) +if "%PLATFORM%"=="" ( + echo Please define 'Platform' environment variable. + exit /B 1 +) + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +set CMAKE_PREFIX_PATH= +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\googletest\install +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\RapidAssist\install +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\win32Arduino\install + +echo ============================================================================ +echo Generating AnyRtttl... +echo ============================================================================ +cd /d %PRODUCT_SOURCE_DIR% +mkdir build >NUL 2>NUL +cd build +cmake -DCMAKE_GENERATOR_PLATFORM=%Platform% -T %PlatformToolset% -DCMAKE_CXX_FLAGS=/D_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" -DANYRTTTL_BUILD_EXAMPLES=ON .. +if %errorlevel% neq 0 exit /b %errorlevel% + +echo ============================================================================ +echo Compiling AnyRtttl... +echo ============================================================================ +cmake --build . --config %Configuration% +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +::Return to launch folder +cd /d %~dp0 diff --git a/lib/AnyRtttl/ci/windows/install_arduinocli.bat b/lib/AnyRtttl/ci/windows/install_arduinocli.bat new file mode 100644 index 0000000..3e1e370 --- /dev/null +++ b/lib/AnyRtttl/ci/windows/install_arduinocli.bat @@ -0,0 +1,31 @@ +@echo off +cd /d %~dp0 + +:: Set download filename +set ARDUINO_CLI_FILENAME=arduino-cli_latest_Windows_64bit.zip + +:: Download +echo Downloading file https://downloads.arduino.cc/arduino-cli/%ARDUINO_CLI_FILENAME% +curl -fsSL -o "%TEMP%\%ARDUINO_CLI_FILENAME%" "https://downloads.arduino.cc/arduino-cli/%ARDUINO_CLI_FILENAME%" +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +:: Installing +set ARDUINO_CLI_INSTALL_DIR=%USERPROFILE%\Desktop\arduino-cli +echo Installing Arduino CLI to directory: %ARDUINO_CLI_INSTALL_DIR% +7z x "%TEMP%\%ARDUINO_CLI_FILENAME%" "-o%ARDUINO_CLI_INSTALL_DIR%" +echo. + +:: Verify +echo Searching for arduino cli executable... +set PATH=%ARDUINO_CLI_INSTALL_DIR%;%PATH% +where arduino-cli.exe +if %errorlevel% neq 0 exit /b %errorlevel% +arduino-cli version +echo. + +echo Installing arduino:avr core... +REM Use `--skip-post-install` on AppVeyor to skip UAC prompt which is blocking the build. +arduino-cli core install arduino:avr --skip-post-install +if %errorlevel% neq 0 exit /b %errorlevel% +echo. diff --git a/lib/AnyRtttl/ci/windows/install_googletest.bat b/lib/AnyRtttl/ci/windows/install_googletest.bat new file mode 100644 index 0000000..8942532 --- /dev/null +++ b/lib/AnyRtttl/ci/windows/install_googletest.bat @@ -0,0 +1,81 @@ +@echo off + +:: Validate mandatory environment variables +if "%CONFIGURATION%"=="" ( + echo Please define 'Configuration' environment variable. + exit /B 1 +) +if "%PLATFORM%"=="" ( + echo Please define 'Platform' environment variable. + exit /B 1 +) + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Prepare CMAKE parameters +set CMAKE_INSTALL_PREFIX=%PRODUCT_SOURCE_DIR%\third_parties\googletest\install +set CMAKE_PREFIX_PATH= +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%; + +echo ============================================================================ +echo Cloning googletest into %PRODUCT_SOURCE_DIR%\third_parties\googletest +echo ============================================================================ +mkdir "%PRODUCT_SOURCE_DIR%\third_parties" >NUL 2>NUL +cd "%PRODUCT_SOURCE_DIR%\third_parties" +git clone "https://github.com/google/googletest.git" +cd googletest +echo. + +echo Checking out version 1.8.0... +git -c advice.detachedHead=false checkout release-1.8.0 +echo. + +echo ============================================================================ +echo Generating googletest... +echo ============================================================================ +mkdir build >NUL 2>NUL +cd build +cmake -Wno-dev -DCMAKE_GENERATOR_PLATFORM=%PLATFORM% -T %PLATFORMTOOLSET% -Dgtest_force_shared_crt=ON -DBUILD_GMOCK=OFF -DBUILD_GTEST=ON -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" .. +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo. +echo. +echo ================================================================================== +echo Patching googletest to silence MSVC warning C4996 about deprecated 'std::tr1' namespace +echo ================================================================================== +echo add_definitions(-D_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING)>>..\googletest\CMakeLists.txt +echo add_definitions(-D_CRT_SECURE_NO_WARNINGS)>>..\googletest\CMakeLists.txt +cmake .. 1>NUL 2>NUL +echo Done patching. +echo. +echo. + +:: Continue with compilation +echo ============================================================================ +echo Compiling googletest... +echo ============================================================================ +cmake --build . --config %CONFIGURATION% -- -maxcpucount /m +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Installing googletest into %CMAKE_INSTALL_PREFIX% +echo ============================================================================ +cmake --build . --config %CONFIGURATION% --target INSTALL +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +::Return to launch folder +cd /d "%~dp0" diff --git a/lib/AnyRtttl/ci/windows/install_rapidassist.bat b/lib/AnyRtttl/ci/windows/install_rapidassist.bat new file mode 100644 index 0000000..dccf3b4 --- /dev/null +++ b/lib/AnyRtttl/ci/windows/install_rapidassist.bat @@ -0,0 +1,68 @@ +@echo off + +:: Validate mandatory environment variables +if "%CONFIGURATION%"=="" ( + echo Please define 'Configuration' environment variable. + exit /B 1 +) +if "%PLATFORM%"=="" ( + echo Please define 'Platform' environment variable. + exit /B 1 +) + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Prepare CMAKE parameters +set CMAKE_INSTALL_PREFIX=%PRODUCT_SOURCE_DIR%\third_parties\RapidAssist\install +set CMAKE_PREFIX_PATH= +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\googletest\install; + +echo ============================================================================ +echo Cloning RapidAssist into %PRODUCT_SOURCE_DIR%\third_parties\RapidAssist +echo ============================================================================ +mkdir "%PRODUCT_SOURCE_DIR%\third_parties" >NUL 2>NUL +cd "%PRODUCT_SOURCE_DIR%\third_parties" +git clone "https://github.com/end2endzone/RapidAssist.git" +cd RapidAssist +echo. + +echo Checking out version v0.10.2... +git -c advice.detachedHead=false checkout 0.10.2 +echo. + +echo ============================================================================ +echo Generating RapidAssist... +echo ============================================================================ +mkdir build >NUL 2>NUL +cd build +cmake -Wno-dev -DCMAKE_GENERATOR_PLATFORM=%PLATFORM% -T %PLATFORMTOOLSET% -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" .. +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Compiling RapidAssist... +echo ============================================================================ +cmake --build . --config %CONFIGURATION% -- -maxcpucount /m +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Installing RapidAssist into %CMAKE_INSTALL_PREFIX% +echo ============================================================================ +cmake --build . --config %CONFIGURATION% --target INSTALL +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +::Return to launch folder +cd /d "%~dp0" diff --git a/lib/AnyRtttl/ci/windows/install_this.bat b/lib/AnyRtttl/ci/windows/install_this.bat new file mode 100644 index 0000000..cebebe3 --- /dev/null +++ b/lib/AnyRtttl/ci/windows/install_this.bat @@ -0,0 +1,46 @@ +@echo off + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Create libraries folder for current user +mkdir %USERPROFILE%\Documents\Arduino\libraries >NUL 2>NUL + +:: Navigate to root directory of repository +cd /d %~dp0 +cd ..\.. + +setlocal + +:: Copy properties as environment variables +FOR /F "tokens=1,2 delims==" %%G IN (library.properties) DO (set %%G=%%H) +echo Installing %name%-%version% for current user + +:: Cleanup +set installdir=%USERPROFILE%\Documents\Arduino\libraries\%name%-%version% +IF EXIST %installdir% ( + rmdir /S /Q %installdir% +) + +:: Copy +xcopy /S /Y %cd% %installdir%\ + +::Cleanup +IF EXIST %installdir%\build ( + rmdir /S /Q %installdir%\build +) +IF EXIST %installdir%\third_parties ( + rmdir /S /Q %installdir%\third_parties +) + +endlocal diff --git a/lib/AnyRtttl/ci/windows/install_win32arduino.bat b/lib/AnyRtttl/ci/windows/install_win32arduino.bat new file mode 100644 index 0000000..4870f7d --- /dev/null +++ b/lib/AnyRtttl/ci/windows/install_win32arduino.bat @@ -0,0 +1,76 @@ +@echo off + +:: Validate mandatory environment variables +if "%CONFIGURATION%"=="" ( + echo Please define 'Configuration' environment variable. + exit /B 1 +) +if "%PLATFORM%"=="" ( + echo Please define 'Platform' environment variable. + exit /B 1 +) + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +:: Prepare CMAKE parameters +set CMAKE_INSTALL_PREFIX=%PRODUCT_SOURCE_DIR%\third_parties\win32Arduino\install +set CMAKE_PREFIX_PATH= +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\googletest\install; +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH%;%PRODUCT_SOURCE_DIR%\third_parties\RapidAssist\install; + +echo ============================================================================ +echo Cloning win32Arduino into %PRODUCT_SOURCE_DIR%\third_parties\win32Arduino +echo ============================================================================ +mkdir "%PRODUCT_SOURCE_DIR%\third_parties" >NUL 2>NUL +cd "%PRODUCT_SOURCE_DIR%\third_parties" +git clone "https://github.com/end2endzone/win32Arduino.git" +cd win32Arduino +echo. + +echo Checking out version 2.4.0... +git -c advice.detachedHead=false checkout 2.4.0 +echo. + +echo ============================================================================ +echo Patching win32Arduino for type __FlashStringHelper... +echo ============================================================================ +python "%PRODUCT_SOURCE_DIR%\ci\generic\patch.py" -i "%PRODUCT_SOURCE_DIR%\third_parties\win32Arduino\include\avr\pgmspace.h" -o "%PRODUCT_SOURCE_DIR%\third_parties\win32Arduino\include\avr\pgmspace.h" -p "%PRODUCT_SOURCE_DIR%\ci\generic\win32arduino.pattern.txt" -r "%PRODUCT_SOURCE_DIR%\ci\generic\win32arduino.replace.txt" +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Generating win32Arduino... +echo ============================================================================ +mkdir build >NUL 2>NUL +cd build +cmake -Wno-dev -DCMAKE_GENERATOR_PLATFORM=%PLATFORM% -T %PLATFORMTOOLSET% -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX="%CMAKE_INSTALL_PREFIX%" -DCMAKE_PREFIX_PATH="%CMAKE_PREFIX_PATH%" .. +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Compiling win32Arduino... +echo ============================================================================ +cmake --build . --config %CONFIGURATION% -- -maxcpucount /m +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +echo ============================================================================ +echo Installing win32Arduino into %PRODUCT_SOURCE_DIR%\third_parties\win32Arduino\install +echo ============================================================================ +cmake --build . --config %CONFIGURATION% --target INSTALL +if %errorlevel% neq 0 exit /b %errorlevel% +echo. + +::Return to launch folder +cd /d "%~dp0" diff --git a/lib/AnyRtttl/ci/windows/test_script.bat b/lib/AnyRtttl/ci/windows/test_script.bat new file mode 100644 index 0000000..d04ff5b --- /dev/null +++ b/lib/AnyRtttl/ci/windows/test_script.bat @@ -0,0 +1,41 @@ +@echo off + +:: Validate mandatory environment variables +if "%CONFIGURATION%"=="" ( + echo Please define 'Configuration' environment variable. + exit /B 1 +) +if "%PLATFORM%"=="" ( + echo Please define 'Platform' environment variable. + exit /B 1 +) + +:: Set PRODUCT_SOURCE_DIR root directory +setlocal enabledelayedexpansion +if "%PRODUCT_SOURCE_DIR%"=="" ( + :: Delayed expansion is required within parentheses https://superuser.com/questions/78496/variables-in-batch-file-not-being-set-when-inside-if + cd /d "%~dp0" + cd ..\.. + set PRODUCT_SOURCE_DIR=!CD! + cd ..\.. + echo PRODUCT_SOURCE_DIR set to '!PRODUCT_SOURCE_DIR!'. +) +endlocal & set PRODUCT_SOURCE_DIR=%PRODUCT_SOURCE_DIR% +echo. + +echo ======================================================================= +echo Running unit tests... +echo ======================================================================= +cd /d "%PRODUCT_SOURCE_DIR%\build\bin\%CONFIGURATION%" +if "%CONFIGURATION%"=="Debug" ( + anyrtttl_unittest-d.exe +) else ( + anyrtttl_unittest.exe +) + +:: Note: +:: GitHub Action do not support uploading test results in a nice GUI. There is no build-in way to detect a failed test. +:: Do not reset the error returned by unit test execution. This will actually fail the build and will indicate in GitHub that a test has failed. +:: +:: Reset error in case of test case fail, this prevents a test failure to actually fail the build +exit /b 0 diff --git a/lib/AnyRtttl/examples.cpp.in b/lib/AnyRtttl/examples.cpp.in new file mode 100644 index 0000000..adbafa7 --- /dev/null +++ b/lib/AnyRtttl/examples.cpp.in @@ -0,0 +1,14 @@ +#include +#include "Arduino.h" +#include "@SOURCE_INO_FILE@" + +using namespace testarduino; + +int main(int argc, char* argv[]) +{ + printf("Calling setup()...\n"); + setup(); + + printf("Calling loop()...\n"); + loop(); +} diff --git a/lib/AnyRtttl/examples/Basic/Basic.ino b/lib/AnyRtttl/examples/Basic/Basic.ino new file mode 100644 index 0000000..4c7ba90 --- /dev/null +++ b/lib/AnyRtttl/examples/Basic/Basic.ino @@ -0,0 +1,22 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + anyrtttl::blocking::play(BUZZER_PIN, tetris); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/BlockingProgramMemoryRtttl/BlockingProgramMemoryRtttl.ino b/lib/AnyRtttl/examples/BlockingProgramMemoryRtttl/BlockingProgramMemoryRtttl.ino new file mode 100644 index 0000000..bf1da79 --- /dev/null +++ b/lib/AnyRtttl/examples/BlockingProgramMemoryRtttl/BlockingProgramMemoryRtttl.ino @@ -0,0 +1,35 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char tetris[] PROGMEM = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char arkanoid[] PROGMEM = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char mario[] PROGMEM = "mario:d=4,o=5,b=140:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; +// James Bond theme defined in inline code below (also stored in flash memory) + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println("ready"); +} + +void loop() { + anyrtttl::blocking::playProgMem(BUZZER_PIN, tetris); + delay(1000); + + anyrtttl::blocking::play_P(BUZZER_PIN, arkanoid); + delay(1000); + + anyrtttl::blocking::play(BUZZER_PIN, FPSTR(mario)); + delay(1000); + + anyrtttl::blocking::play(BUZZER_PIN, F("Bond:d=4,o=5,b=80:32p,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d#6,16d#6,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d6,16c#6,16c#7,c.7,16g#6,16f#6,g#.6")); + delay(1000); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/BlockingRtttl/BlockingRtttl.ino b/lib/AnyRtttl/examples/BlockingRtttl/BlockingRtttl.ino new file mode 100644 index 0000000..6156ecd --- /dev/null +++ b/lib/AnyRtttl/examples/BlockingRtttl/BlockingRtttl.ino @@ -0,0 +1,31 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char * arkanoid = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char * mario = "mario:d=4,o=5,b=140:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println("ready"); +} + +void loop() { + anyrtttl::blocking::play(BUZZER_PIN, tetris); + delay(1000); + + anyrtttl::blocking::play(BUZZER_PIN, arkanoid); + delay(1000); + + anyrtttl::blocking::play(BUZZER_PIN, mario); + delay(1000); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/BlockingWithNonBlocking/BlockingWithNonBlocking.ino b/lib/AnyRtttl/examples/BlockingWithNonBlocking/BlockingWithNonBlocking.ino new file mode 100644 index 0000000..9f6e4d0 --- /dev/null +++ b/lib/AnyRtttl/examples/BlockingWithNonBlocking/BlockingWithNonBlocking.ino @@ -0,0 +1,44 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char * arkanoid = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char * mario = "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + + // Play tetris and wait until done playing before jumping to the next song. + anyrtttl::nonblocking::begin(BUZZER_PIN, tetris); + while( !anyrtttl::nonblocking::done() ) + { + anyrtttl::nonblocking::play(); + } + + // Play arkanoid and loop until done playing before jumping to the next song. + anyrtttl::nonblocking::begin(BUZZER_PIN, arkanoid); + while( !anyrtttl::nonblocking::done() ) + { + anyrtttl::nonblocking::play(); + } + + // Play mario and loop until done playing before looping again. + anyrtttl::nonblocking::begin(BUZZER_PIN, mario); + while( !anyrtttl::nonblocking::done() ) + { + anyrtttl::nonblocking::play(); + } + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/NonBlockingProgramMemoryRtttl/NonBlockingProgramMemoryRtttl.ino b/lib/AnyRtttl/examples/NonBlockingProgramMemoryRtttl/NonBlockingProgramMemoryRtttl.ino new file mode 100644 index 0000000..e12f033 --- /dev/null +++ b/lib/AnyRtttl/examples/NonBlockingProgramMemoryRtttl/NonBlockingProgramMemoryRtttl.ino @@ -0,0 +1,41 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char tetris[] PROGMEM = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char arkanoid[] PROGMEM = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char mario[] PROGMEM = "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; +// James Bond theme defined in inline code below (also stored in flash memory) +byte songIndex = 0; //which song to play when the previous one finishes + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + // If we are not playing something + if ( !anyrtttl::nonblocking::isPlaying() ) + { + // Play a song based on songIndex. + if (songIndex == 0) + anyrtttl::nonblocking::beginProgMem(BUZZER_PIN, tetris); + else if (songIndex == 1) + anyrtttl::nonblocking::begin_P(BUZZER_PIN, arkanoid); + else if (songIndex == 2) + anyrtttl::nonblocking::begin(BUZZER_PIN, FPSTR(mario)); + else if (songIndex == 3) + anyrtttl::nonblocking::begin(BUZZER_PIN, F("Bond:d=4,o=5,b=80:32p,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d#6,16d#6,16c#6,32d#6,32d#6,16d#6,8d#6,16c#6,16c#6,16c#6,16c#6,32e6,32e6,16e6,8e6,16d#6,16d6,16c#6,16c#7,c.7,16g#6,16f#6,g#.6")); + + //Set songIndex ready for next song + songIndex++; + } + else + { + anyrtttl::nonblocking::play(); + } +} diff --git a/lib/AnyRtttl/examples/NonBlockingRtttl/NonBlockingRtttl.ino b/lib/AnyRtttl/examples/NonBlockingRtttl/NonBlockingRtttl.ino new file mode 100644 index 0000000..fac679b --- /dev/null +++ b/lib/AnyRtttl/examples/NonBlockingRtttl/NonBlockingRtttl.ino @@ -0,0 +1,38 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; +const char * arkanoid = "Arkanoid:d=4,o=5,b=140:8g6,16p,16g.6,2a#6,32p,8a6,8g6,8f6,8a6,2g6"; +const char * mario = "mario:d=4,o=5,b=100:16e6,16e6,32p,8e6,16c6,8e6,8g6,8p,8g,8p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,16p,8c6,16p,8g,16p,8e,16p,8a,8b,16a#,8a,16g.,16e6,16g6,8a6,16f6,8g6,8e6,16c6,16d6,8b,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16c7,16p,16c7,16c7,p,16g6,16f#6,16f6,16d#6,16p,16e6,16p,16g#,16a,16c6,16p,16a,16c6,16d6,8p,16d#6,8p,16d6,8p,16c6"; +byte songIndex = 0; //which song to play when the previous one finishes + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + // If we are not playing something + if ( !anyrtttl::nonblocking::isPlaying() ) + { + // Play a song based on songIndex. + if (songIndex == 0) + anyrtttl::nonblocking::begin(BUZZER_PIN, tetris); + else if (songIndex == 1) + anyrtttl::nonblocking::begin(BUZZER_PIN, arkanoid); + else if (songIndex == 2) + anyrtttl::nonblocking::begin(BUZZER_PIN, mario); + + //Set songIndex ready for next song + songIndex++; + } + else + { + anyrtttl::nonblocking::play(); + } +} diff --git a/lib/AnyRtttl/examples/NonBlockingStopBeforeEnd/NonBlockingStopBeforeEnd.ino b/lib/AnyRtttl/examples/NonBlockingStopBeforeEnd/NonBlockingStopBeforeEnd.ino new file mode 100644 index 0000000..7cffd2c --- /dev/null +++ b/lib/AnyRtttl/examples/NonBlockingStopBeforeEnd/NonBlockingStopBeforeEnd.ino @@ -0,0 +1,42 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; + +unsigned long playStart = 0; +bool firstPass = true; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + if (firstPass) { + anyrtttl::nonblocking::begin(BUZZER_PIN, tetris); + + //remember when we started playing the song + playStart = millis(); + + firstPass = false; + } + + //if we are playing something + if ( anyrtttl::nonblocking::isPlaying() ) { + + //does the melody been playing for more than 5 seconds ? + if ( millis() - playStart > 5000 ) + { + anyrtttl::nonblocking::stop(); + } + } + + //if anything available for playing, play it + anyrtttl::nonblocking::play(); + +} diff --git a/lib/AnyRtttl/examples/Play10Bits/Play10Bits.ino b/lib/AnyRtttl/examples/Play10Bits/Play10Bits.ino new file mode 100644 index 0000000..841ee7c --- /dev/null +++ b/lib/AnyRtttl/examples/Play10Bits/Play10Bits.ino @@ -0,0 +1,44 @@ +#include +#include +#include + +//The BitReader library is required for extracting 10 bit blocks from the RTTTL buffer. +//It can be installed from Arduino Library Manager or from https://github.com/end2endzone/BitReader/releases +#include + +//project's constants +#define BUZZER_PIN 8 + +//RTTTL 10 bits binary format for the following: tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a +const unsigned char tetris10[] = {0x0A, 0x14, 0x12, 0xCE, 0x34, 0xE0, 0x82, 0x14, 0x32, 0x38, 0xE0, 0x4C, 0x2A, 0xAD, 0x34, 0xA0, 0x84, 0x0B, 0x0E, 0x28, 0xD3, 0x4C, 0x03, 0x2A, 0x28, 0xA1, 0x80, 0x2A, 0xA5, 0xB4, 0x93, 0x82, 0x1B, 0xAA, 0x38, 0xE2, 0x86, 0x12, 0x4E, 0x38, 0xA0, 0x84, 0x0B, 0x0E, 0x28, 0xD3, 0x4C, 0x03, 0x2A, 0x28, 0xA1, 0x80, 0x2A, 0xA9, 0x04}; +const int tetris10_length = 42; + +//bit reader support +#ifndef USE_BITADDRESS_READ_WRITE +BitReader bitreader; +#else +BitAddress bitreader; +#endif +uint16_t readNextBits(uint8_t numBits) +{ + uint16_t bits = 0; + bitreader.read(numBits, &bits); + return bits; +} + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + bitreader.setBuffer(tetris10); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + anyrtttl::blocking::play10Bits(BUZZER_PIN, tetris10_length, &readNextBits); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/Play16Bits/Play16Bits.ino b/lib/AnyRtttl/examples/Play16Bits/Play16Bits.ino new file mode 100644 index 0000000..ad18800 --- /dev/null +++ b/lib/AnyRtttl/examples/Play16Bits/Play16Bits.ino @@ -0,0 +1,25 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 + +//RTTTL 16 bits binary format for the following: tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a +const unsigned char tetris16[] = {0x0A, 0x14, 0x12, 0x02, 0x33, 0x01, 0x03, 0x02, 0x0B, 0x02, 0x14, 0x02, 0x0C, 0x02, 0x03, 0x02, 0x33, 0x01, 0x2A, 0x01, 0x2B, 0x01, 0x03, 0x02, 0x12, 0x02, 0x0B, 0x02, 0x03, 0x02, 0x32, 0x01, 0x33, 0x01, 0x03, 0x02, 0x0A, 0x02, 0x12, 0x02, 0x02, 0x02, 0x2A, 0x01, 0x29, 0x01, 0x3B, 0x01, 0x0A, 0x02, 0x1B, 0x02, 0x2A, 0x02, 0x23, 0x02, 0x1B, 0x02, 0x12, 0x02, 0x13, 0x02, 0x03, 0x02, 0x12, 0x02, 0x0B, 0x02, 0x03, 0x02, 0x32, 0x01, 0x33, 0x01, 0x03, 0x02, 0x0A, 0x02, 0x12, 0x02, 0x02, 0x02, 0x2A, 0x01, 0x2A, 0x01}; +const int tetris16_length = 42; + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); +} + +void loop() { + anyrtttl::blocking::play16Bits(BUZZER_PIN, tetris16, tetris16_length); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/examples/Rtttl2Code/Rtttl2Code.ino b/lib/AnyRtttl/examples/Rtttl2Code/Rtttl2Code.ino new file mode 100644 index 0000000..afb3c30 --- /dev/null +++ b/lib/AnyRtttl/examples/Rtttl2Code/Rtttl2Code.ino @@ -0,0 +1,52 @@ +#include +#include +#include + +//project's constants +#define BUZZER_PIN 8 +const char * tetris = "tetris:d=4,o=5,b=160:e6,8b,8c6,8d6,16e6,16d6,8c6,8b,a,8a,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,2a,8p,d6,8f6,a6,8g6,8f6,e6,8e6,8c6,e6,8d6,8c6,b,8b,8c6,d6,e6,c6,a,a"; + +//******************************************************************************************************************* +// The following replacement functions prints the function call & parameters to the serial port. +//******************************************************************************************************************* +void serialTone(uint8_t pin, unsigned int frequency, unsigned long duration) { + Serial.print("tone("); + Serial.print(pin); + Serial.print(","); + Serial.print(frequency); + Serial.print(","); + Serial.print(duration); + Serial.println(");"); +} + +void serialNoTone(uint8_t pin) { + Serial.print("noTone("); + Serial.print(pin); + Serial.println(");"); +} + +void serialDelay(unsigned long duration) { + Serial.print("delay("); + Serial.print(duration); + Serial.println(");"); +} + +void setup() { + pinMode(BUZZER_PIN, OUTPUT); + + Serial.begin(115200); + Serial.println(); + + //Use custom functions + anyrtttl::setToneFunction(&serialTone); + anyrtttl::setNoToneFunction(&serialNoTone); + anyrtttl::setDelayFunction(&serialDelay); +} + +void loop() { + anyrtttl::blocking::play(BUZZER_PIN, tetris); + + while(true) + { + } +} diff --git a/lib/AnyRtttl/keywords.txt b/lib/AnyRtttl/keywords.txt new file mode 100644 index 0000000..d327e0e --- /dev/null +++ b/lib/AnyRtttl/keywords.txt @@ -0,0 +1,16 @@ +anyrtttl KEYWORD1 +play16Bits KEYWORD2 +play10Bits KEYWORD2 +begin KEYWORD2 +beginProgMem KEYWORD2 +begin_P KEYWORD2 +play KEYWORD2 +playProgMem KEYWORD2 +play_P KEYWORD2 +stop KEYWORD2 +isPlaying KEYWORD2 +done KEYWORD2 +setToneFunction KEYWORD2 +setNoToneFunction KEYWORD2 +setDelayFunction KEYWORD2 +setMillisFunction KEYWORD2 diff --git a/lib/AnyRtttl/library.properties b/lib/AnyRtttl/library.properties new file mode 100644 index 0000000..f8c77a3 --- /dev/null +++ b/lib/AnyRtttl/library.properties @@ -0,0 +1,9 @@ +name=AnyRtttl +version=2.3 +author=Antoine Beauchamp +maintainer=Antoine Beauchamp +sentence=A feature rich arduino library for playing rtttl melodies. +paragraph=The AnyRtttl is a feature rich library which supports all best RTTTL features: Blocking & Non-Blocking modes, custom tone(), delay() and millis() functions, PROGMEM support, and much more. +category=Other +url=https://github.com/end2endzone/AnyRtttl +architectures=* diff --git a/lib/AnyRtttl/src/anyrtttl.cpp b/lib/AnyRtttl/src/anyrtttl.cpp new file mode 100644 index 0000000..df8ea12 --- /dev/null +++ b/lib/AnyRtttl/src/anyrtttl.cpp @@ -0,0 +1,672 @@ +// --------------------------------------------------------------------------- +// AUTHOR/LICENSE: +// The following code was written by Antoine Beauchamp. For other authors, see AUTHORS file. +// The code & updates for the library can be found at https://github.com/end2endzone/AnyRtttl +// MIT License: http://www.opensource.org/licenses/mit-license.php +// --------------------------------------------------------------------------- +#include "Arduino.h" +#include "anyrtttl.h" +#include "binrtttl.h" + +/********************************************************* + * RTTTL Library data + *********************************************************/ + +namespace anyrtttl +{ + +const uint16_t notes[] = { NOTE_SILENT, +NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4, +NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5, +NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6, +NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7 +}; + +#define isdigit(n) (n >= '0' && n <= '9') +typedef uint16_t TONE_DURATION; +static const byte NOTES_PER_OCTAVE = 12; + +const char * buffer = ""; +ReadCharFuncPtr readCharFunc = &readChar; +int bufferIndex = -32760; +byte default_dur = 4; +byte default_oct = 5; +RTTTL_BPM bpm = 63; +RTTTL_DURATION wholenote; +byte pin = -1; +unsigned long delayToNextNote = 0; //milliseconds before playing the next note +bool playing = false; +TONE_DURATION duration; +byte noteOffset; +RTTTL_OCTAVE_VALUE scale; +int tmpNumber; + +const char * readNumber(const char * iBuffer, int & oValue, ReadCharFuncPtr iReadCharFunc) +{ + oValue = 0; + while(isdigit(iReadCharFunc(iBuffer))) + { + oValue = (oValue * 10) + (readCharFunc(buffer) - '0'); + iBuffer++; + } + return iBuffer; +} + +void serialPrint(const char * iBuffer, ReadCharFuncPtr iReadCharFunc) +{ + char c = readCharFunc(iBuffer); + while(c) { + Serial.print(c); + iBuffer++; + c = readCharFunc(iBuffer); + } +} + +/**************************************************************************** + * Custom functions + ****************************************************************************/ + +ToneFuncPtr _tone = &tone; +NoToneFuncPtr _noTone = &noTone; +DelayFuncPtr _delay = &delay; +MillisFuncPtr _millis = &millis; + +void setToneFunction(ToneFuncPtr iFunc) { + _tone = iFunc; +} + +void setNoToneFunction(NoToneFuncPtr iFunc) { + _noTone = iFunc; +} + +void setDelayFunction(DelayFuncPtr iFunc) { + _delay = iFunc; +} + +void setMillisFunction(MillisFuncPtr iFunc) { + _millis = iFunc; +} + +char readChar(const char * iBuffer) { + return *iBuffer; +} + +char readChar_P(const char * iBuffer) { + return pgm_read_byte_near(iBuffer); +} + + + +/**************************************************************************** + * Blocking API + ****************************************************************************/ +namespace blocking +{ + +void play(byte iPin, const char * iBuffer, ReadCharFuncPtr iReadCharFunc) { + // Absolutely no error checking in here + + default_dur = 4; + default_oct = 6; + bpm = 63; + buffer = iBuffer; + readCharFunc = iReadCharFunc; + + #ifdef ANY_RTTTL_DEBUG + Serial.print("playing: "); + serialPrint(buffer, readCharFunc); + Serial.println(); + #endif + + // format: d=N,o=N,b=NNN: + // find the start (skip name, etc) + + while(readCharFunc(buffer) != ':') buffer++; // ignore name + buffer++; // skip ':' + + // get default duration + if(readCharFunc(buffer) == 'd') + { + buffer++; buffer++; // skip "d=" + buffer = readNumber(buffer, tmpNumber, readCharFunc); + if(tmpNumber > 0) + default_dur = tmpNumber; + buffer++; // skip comma + } + + #ifdef ANY_RTTTL_INFO + Serial.print("ddur: "); Serial.println(default_dur, 10); + #endif + + // get default octave + if(readCharFunc(buffer) == 'o') + { + buffer++; buffer++; // skip "o=" + buffer = readNumber(buffer, tmpNumber, readCharFunc); + if(tmpNumber >= 3 && tmpNumber <= 7) + default_oct = tmpNumber; + buffer++; // skip comma + } + + #ifdef ANY_RTTTL_INFO + Serial.print("doct: "); Serial.println(default_oct, 10); + #endif + + // get BPM + if(readCharFunc(buffer) == 'b') + { + buffer++; buffer++; // skip "b=" + buffer = readNumber(buffer, tmpNumber, readCharFunc); + bpm = tmpNumber; + buffer++; // skip colon + } + + #ifdef ANY_RTTTL_INFO + Serial.print("bpm: "); Serial.println(bpm, 10); + #endif + + // BPM usually expresses the number of quarter notes per minute + wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole noteOffset (in milliseconds) + + #ifdef ANY_RTTTL_INFO + Serial.print("wn: "); Serial.println(wholenote, 10); + #endif + + // now begin note loop + while(readCharFunc(buffer)) + { + // first, get note duration, if available + buffer = readNumber(buffer, tmpNumber, readCharFunc); + + if(tmpNumber) + duration = wholenote / tmpNumber; + else + duration = wholenote / default_dur; // we will need to check if we are a dotted noteOffset after + + // now get the note + noteOffset = getNoteOffsetFromLetter(readCharFunc(buffer)); + buffer++; + + // now, get optional '#' sharp + if(readCharFunc(buffer) == '#') + { + noteOffset++; + buffer++; + } + + // now, get optional '.' dotted note + if(readCharFunc(buffer) == '.') + { + duration += duration/2; + buffer++; + } + + // now, get scale + if(isdigit(readCharFunc(buffer))) + { + scale = readCharFunc(buffer) - '0'; + buffer++; + } + else + { + scale = default_oct; + } + + if(readCharFunc(buffer) == ',') + buffer++; // skip comma for next note (or we may be at the end) + + // now play the note + if(noteOffset) + { + uint16_t frequency = notes[(scale - 4) * NOTES_PER_OCTAVE + noteOffset]; + + #ifdef ANY_RTTTL_INFO + Serial.print("Playing: "); + Serial.print(scale, 10); Serial.print(' '); + Serial.print(noteOffset, 10); Serial.print(" ("); + Serial.print(frequency, 10); + Serial.print(") "); + Serial.println(duration, 10); + #endif + + _tone(iPin, frequency, duration); + _delay(duration+1); + _noTone(iPin); + } + else + { + #ifdef ANY_RTTTL_INFO + Serial.print("Pausing: "); + Serial.println(duration, 10); + #endif + _delay(duration); + } + } +} + +void play(byte iPin, const char * iBuffer) { play(iPin, iBuffer, &readChar); } + +void play(byte iPin, const __FlashStringHelper* str) { play(iPin, (const char *)str, &readChar_P); } +void playProgMem(byte iPin, const char * iBuffer) { play(iPin, iBuffer, &readChar_P); } +void play_P(byte iPin, const char * iBuffer) { play(iPin, iBuffer, &readChar_P); } +void play_P(byte iPin, const __FlashStringHelper* str) { play(iPin, (const char *)str, &readChar_P); } + +void play16Bits(int iPin, const unsigned char * iBuffer, int iNumNotes) { + // Absolutely no error checking in here + + RTTTL_DEFAULT_VALUE_SECTION * defaultSection = (RTTTL_DEFAULT_VALUE_SECTION *)iBuffer; + RTTTL_NOTE * notesBuffer = (RTTTL_NOTE *)iBuffer; + + bpm = defaultSection->bpm; + + #ifdef ANY_RTTTL_DEBUG + Serial.print("numNotes="); + Serial.println(iNumNotes); + // format: d=N,o=N,b=NNN: + Serial.print("d="); + Serial.print(getNoteDurationFromIndex(defaultSection->durationIdx)); + Serial.print(",o="); + Serial.print(getNoteOctaveFromIndex(defaultSection->octaveIdx)); + Serial.print(",b="); + Serial.println(bpm); + #endif + + // BPM usually expresses the number of quarter notes per minute + wholenote = (60 * 1000L / bpm) * 4; // this is the time for whole noteOffset (in milliseconds) + + // now begin note loop + for(int i=0; i= 0 && iIndex < gNoteLettersCount) + return gNoteLetters[iIndex]; + return -1; +} + +uint16_t getNoteLettersCount() +{ + return gNoteLettersCount; +} + +NOTE_LETTER_INDEX findNoteLetterIndex(RTTTL_NOTE_LETTER n) +{ + for(NOTE_LETTER_INDEX i=0; i= 0 && iIndex < gNoteLettersCount) + return gNoteOffsets[iIndex]; + return 0; +} + +int getNoteOffsetFromLetter(RTTTL_NOTE_LETTER n) +{ + NOTE_LETTER_INDEX index = findNoteLetterIndex(n); + return getNoteOffsetFromLetterIndex(index); +} + +RTTTL_DURATION getNoteDurationFromIndex(DURATION_INDEX iIndex) +{ + if (iIndex >= 0 && iIndex < gNoteDurationsCount) + return gNoteDurations[iIndex]; + return -1; +} + +uint16_t getNoteDurationsCount() +{ + return gNoteDurationsCount; +} + +DURATION_INDEX findNoteDurationIndex(RTTTL_DURATION n) +{ + for(DURATION_INDEX i=0; i= 0 && iIndex < gNoteOctavesCount) + return gNoteOctaves[iIndex]; + return -1; +} + +uint16_t getNoteOctavesCount() +{ + return gNoteOctavesCount; +} + +OCTAVE_INDEX findNoteOctaveIndex(RTTTL_OCTAVE_VALUE n) +{ + for(OCTAVE_INDEX i=0; i= 0 && iIndex < gNoteBpmsCount) + return gNoteBpms[iIndex]; + return -1; +} + +uint16_t getBpmsCount() +{ + return gNoteBpmsCount; +} + +BPM_INDEX findBpmIndex(RTTTL_BPM n) +{ + for(BPM_INDEX i=0; i THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/partitions_8mb_ota_2mb_ee.cvs b/partitions_8mb_ota_2mb_ee.cvs new file mode 100644 index 0000000..acb6073 --- /dev/null +++ b/partitions_8mb_ota_2mb_ee.cvs @@ -0,0 +1,9 @@ +#apps(2.75MB), spiffs(2.5MB) +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x2C0000, +app1, app, ota_1, 0x2D0000, 0x2C0000, +eeprom, data, 0x99, 0x590000, 0x1000, +coredump, data, coredump,0x591000, 0x10000, +spiffs, data, spiffs, 0x5A1000, 0x25F000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..968b6db --- /dev/null +++ b/platformio.ini @@ -0,0 +1,39 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32s3dev] +platform = espressif32 +board = wroom-32S3-N8R2 +framework = arduino +board_build.filesystem = littlefs +board_build.partitions = partitions_8mb_ota_2mb_ee.cvs +monitor_speed = 115200 +monitor_filters = esp32_exception_decoder +board_build.sdkconfig_defaults = sdkconfig.defaults +lib_deps = + bblanchon/ArduinoJson + makuna/NeoPixelBus @ ^2.8.3 + https://github.com/me-no-dev/ESPAsyncWebServer + jeremycole/I2C Temperature Sensors derived from the LM75 @ ^1.0.3 + mathertel/OneButton @ ^2.6.1 + h2zero/NimBLE-Arduino @ ^1.4.1 + adafruit/Adafruit SSD1306 @ ^2.5.7 + fastled/FastLED @ ^3.9.4 + marian-craciunescu/ESP32Ping@^1.7 + + +build_flags = + -mfix-esp32-psram-cache-issue + -D CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=1 + -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE + -D CONFIG_ARDUHAL_LOG_COLORS=1 +upload_port = COM11 +debug_init_break = tbreak setup +monitor_port = COM11 diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..fbca3b6 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1 @@ +CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=y \ No newline at end of file diff --git a/src/ATALights.cpp b/src/ATALights.cpp new file mode 100644 index 0000000..a0276f8 --- /dev/null +++ b/src/ATALights.cpp @@ -0,0 +1,478 @@ +#include "ATALights.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include +#include +#include "Animations.h" +#include +#include "system.h" +#include "ColorPalettes.h" +//#include + +#define FASTLED_CORE 0 +static const char* tag = "strips"; + +TaskHandle_t Animation_Task_Handle; +LEDSTRIP_SETTINGS ledSettings[2]; +volatile bool AnimationLooping = false; +ANIM_EVENT prevAnimEvent = {0}; +QueueHandle_t animationQueue = xQueueCreate( 1, sizeof( ANIM_EVENT ) ); + +void Lights_Set_Animation(int animIndex, uint8_t red, uint8_t grn, uint8_t blu){ + + // Trigger to exit curring looping animation + if(AnimationLooping){ + AnimationLooping = false; + xTaskNotifyGive( Animation_Task_Handle ); + } + + ANIM_EVENT event; + event.AnimationIndex = animIndex; + event.data.red = red; + event.data.grn = grn; + event.data.blu = blu; + event.data.wht = 0; + xQueueSend( animationQueue, &event, 25 ); +} + +void Lights_Set_ON(){ + Lights_Set_Animation(prevAnimEvent.AnimationIndex, prevAnimEvent.data.red, prevAnimEvent.data.grn, prevAnimEvent.data.blu); +} + +void Lights_Set_OFF(){ + Lights_Set_Animation(OFF_INDEX, 0, 0, 0); +} + +void Lights_Set_Brightness(uint8_t scale){ + FastLED.setBrightness(scale); +} + +void Lights_Set_White(uint8_t val){ + //pwmOut[0]->setOutput(led_status.white / 2.5f); +} + +void Init_Lights_Task(void){ + + ledSettings[0].leds = new CRGB[ledSettings[0].size]; + Init_Strip(ledSettings[0].leds, ledSettings[0].pin, ledSettings[0].size, ledSettings[0].rgbOrder, ledSettings[0].chip, ledSettings[0].bright); + ESP_LOGD(tag, "Initializing Strip1: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[0].pin, ledSettings[0].size, ledSettings[0].rgbOrder, ledSettings[0].chip); + + + ledSettings[1].leds = new CRGB[ledSettings[1].size];; + Init_Strip(ledSettings[1].leds, ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip, ledSettings[1].bright); + ESP_LOGD(tag, "Initializing Strip2: Pin: %d, size: %d, order: %s, chip: %s", ledSettings[1].pin, ledSettings[1].size, ledSettings[1].rgbOrder, ledSettings[1].chip); + + xTaskCreatePinnedToCore(Lights_Control_Task, "Lights_Task", 1024*8, NULL, 1, &Animation_Task_Handle, FASTLED_CORE); + ESP_LOGI(tag, "Lights Task Created..."); +} + + +void Animation_Loop_Exit(void){ + if( Animation_Task_Handle ){ + xTaskNotifyGive( Animation_Task_Handle ); + } +} + + +EOrder GetEOrderByString(const String& rgbStr){ + ESP_LOGI(tag, "EOrder is %s", rgbStr.c_str()); + if (rgbStr.equalsIgnoreCase("RGB")) return RGB; + else if (rgbStr.equalsIgnoreCase("RBG")) return RBG; + else if (rgbStr.equalsIgnoreCase("GRB")) return GRB; + else if (rgbStr.equalsIgnoreCase("GBR")) return GBR; + else if (rgbStr.equalsIgnoreCase("BRG")) return BRG; + else if (rgbStr.equalsIgnoreCase("BGR")) return BGR; + else return RGB; // Default case +} + + +void LightsON(void){ + FastLED.show(); +} + + +void LightsOff(void){ + FastLED.clear(); + FastLED.show(); +} + + +/* +void setPixel(LEDSTRIP_SETTINGS& strip, int pixelIndex, CRGB col){ + int x = calcPixelIndex(strip, pixelIndex); + leds1[x] = col; +} + +inline int calcPixelIndex(LEDSTRIP_SETTINGS& strip, int index) { + int x = (index + strip.shift) % strip.effSize; + x = (x < 0) ? (x + strip.effSize) : x; + return (x +strip.offset) % strip.effSize; +} +*/ + +inline void setPixel1(LEDSTRIP_SETTINGS& leds, int pixelIndex, const CRGB col) { + register uint16_t x = pixelIndex + ledSettings[0].shift; + // If strip.effSize is power of 2, use faster bit masking + if ((leds.effSize & (leds.effSize - 1)) == 0) { + x = (x < 0) ? ((x + leds.effSize) & (leds.effSize - 1)) : (x & (leds.effSize - 1)); + leds.leds[(x + leds.offset) & (leds.effSize - 1)] = col; + } else { + // For non-power-of-2 sizes, still need modulo + x = (x < 0) ? ((x + ledSettings[0].effSize) % ledSettings[0].effSize) : (x % ledSettings[0].effSize); + ledSettings[0].leds[(x + ledSettings[0].offset) % ledSettings[0].effSize] = col; + } +} + + +inline void setPixel2(int pixelIndex, const CRGB col) { + register uint16_t x = pixelIndex + ledSettings[1].shift; + // If strip.effSize is power of 2, use faster bit masking + if ((ledSettings[1].effSize & (ledSettings[1].effSize - 1)) == 0) { + x = (x < 0) ? ((x + ledSettings[1].effSize) & (ledSettings[1].effSize - 1)) : (x & (ledSettings[1].effSize - 1)); + ledSettings[1].leds[(x + ledSettings[1].offset) & (ledSettings[1].effSize - 1)] = col; + } else { + // For non-power-of-2 sizes, still need modulo + x = (x < 0) ? ((x + ledSettings[1].effSize) % ledSettings[1].effSize) : (x % ledSettings[1].effSize); + ledSettings[1].leds[(x + ledSettings[1].offset) % ledSettings[1].effSize] = col; + } +} + + +void Init_Strip(CRGB* leds, uint8_t pin, int size, const String& colorOrder, const String& chipType, uint8_t bright) { + // Validate inputs + if (!leds || size <= 0) { + ESP_LOGE(tag, "Invalid LED array or size"); + return; + } + + // Convert strings to uppercase once + String chipUpper = chipType; + String orderUpper = colorOrder; + chipUpper.toUpperCase(); + orderUpper.toUpperCase(); + + // Validate pin (using AND for correct logic) + if (pin != 3 && pin != 9 && pin != 46) { + ESP_LOGW(tag, "Invalid pin %d, defaulting to 3", pin); + pin = 3; + } + + EOrder rgbOrder = GetEOrderByString(orderUpper); + + if(pin == 3){ + // First level: Chip type selection + if (chipUpper == "WS2812B" || chipUpper == "SK6812") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "SK6812") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2811_400") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2815") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else { + // Default to WS2812B if unknown chip type + ESP_LOGW(tag, "Unknown LED chip type: %s, defaulting to WS2812B", chipType.c_str()); + FastLED.addLeds(leds, size); + } + ESP_LOGI(tag, "Initialized %s LED strip with %d LEDs on pin %d", chipType.c_str(), size, pin); + } + else if(pin == 9){ + // First level: Chip type selection + if (chipUpper == "WS2812B") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "SK6812") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2811_400") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2815") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else { + // Default to WS2812B if unknown chip type + ESP_LOGW(tag, "Unknown LED chip type: %s, defaulting to WS2812B", chipType.c_str()); + FastLED.addLeds(leds, size); + } + ESP_LOGI(tag, "Initialized %s LED strip with %d LEDs on pin %d", chipType.c_str(), size, pin); + } + else if(pin == 46){ + // First level: Chip type selection + if (chipUpper == "WS2812B" || chipUpper == "SK6812") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "SK6812") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2811_400") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else if (chipUpper == "WS2815") { + switch(rgbOrder) { + case RGB: FastLED.addLeds(leds, size); break; + case RBG: FastLED.addLeds(leds, size); break; + case GRB: FastLED.addLeds(leds, size); break; + case GBR: FastLED.addLeds(leds, size); break; + case BRG: FastLED.addLeds(leds, size); break; + case BGR: FastLED.addLeds(leds, size); break; + default: FastLED.addLeds(leds, size); break; + } + } + else { + // Default to WS2812B if unknown chip type + ESP_LOGW(tag, "Unknown LED chip type: %s, defaulting to WS2812B", chipType.c_str()); + FastLED.addLeds(leds, size); + } + ESP_LOGI(tag, "Initialized %s LED strip with %d LEDs on pin %d", chipType.c_str(), size, pin); + } + + FastLED.setBrightness(bright); +} + + +void Lights_Control_Task_Resume(void){ + vTaskResume(Animation_Task_Handle); +} + + +void Lights_Control_Task(void *parameters){ + + ANIM_EVENT AnimEvent; + CRGB col; + COLOR_PACK colorPack; + CRGBPalette16 firePalette; + + ESP_LOGD(tag, "Lights Control Task Entered..."); + vTaskSuspend(NULL); + ESP_LOGD(tag, "Lights Control Task Resumed..."); + vTaskDelay(1000); + + Lights_Set_Brightness(48); + Lights_Set_Animation(1, 0, 0, 0); // set rainbow animation + + + while (true) { + if (xQueueReceive(animationQueue, &AnimEvent, portMAX_DELAY) == pdTRUE) { + ESP_LOGD(tag, "New Animation Event: Index: %d", AnimEvent.AnimationIndex); + switch (AnimEvent.AnimationIndex) { + case -3: // Set Pixel by index + ledSettings[0].leds[AnimEvent.data.data[7]] = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu); + FastLED.show(); + break; + case -2: // Fill Static Color + col = CRGB(AnimEvent.data.red, AnimEvent.data.grn, AnimEvent.data.blu); + fill_solid(ledSettings[0].leds, ledSettings[0].size, col); + FastLED.show(); + ESP_LOGD(tag, "Static Color"); + break; + case -1: + FastLED.clear(); + FastLED.show(); + ESP_LOGD(tag, "LEDs Off"); + break; + case 0: + break; + case 1: + Anim_Rainbow(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 30); + break; + case 2: + Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 1000, 0); + break; + case 3: + Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 2000, 0); + break; + case 4: + Anim_TimedFill(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, CRGB::Black, CRGB::White, 3000, 0); + break; + case 5: + createFirePalette(firePalette, CRGB::Red, CRGB::OrangeRed, CRGB::Orange); + Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0); + break; + case 6: + createFirePalette(firePalette, CRGB::DarkGreen, CRGB::Green, CRGB::LightGreen); + Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0); + break; + case 7: + createFirePalette(firePalette, CRGB::DarkBlue, CRGB::Blue, CRGB::LightBlue); + Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0); + break; + case 8: + createFirePalette(firePalette, CRGB::Purple, CRGB::Blue, CRGB::Violet); + Anim_Fire(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, 60, firePalette, 0); + break; + case 9: + loadColorPack(colorPack, colorPack_USA); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80); + break; + case 10: + loadColorPack(colorPack, colorPack_MEXICO); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 1, 80); + break; + case 11: + loadColorPack(colorPack, colorPack_RedBlack); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80); + break; + case 12: + loadColorPack(colorPack, colorPack_YellowBlack); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80); + break; + case 13: + loadColorPack(colorPack, colorPack_GreenBlack); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80); + break; + case 14: + loadColorPack(colorPack, colorPack_BlueBlack); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80); + break; + case 15: + loadColorPack(colorPack, colorPack_VioletBlack); + Anim_Color_Sectors(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 4, 80); + break; + case 16: + loadColorPack(colorPack, colorPack_RAINBOW); + Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1); + break; + case 17: + loadColorPack(colorPack, colorPack_USA); + Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, LINEAR_DECAY, true, 1); + break; + case 18: + loadColorPack(colorPack, colorPack_USA); + Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, false, 1); + break; + case 19: + loadColorPack(colorPack, colorPack_USA); + Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 2); + break; + case 20: + loadColorPack(colorPack, colorPack_RAINBOW); + Anim_Comets(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 80, RANDOM_DECAY, true, 1); + break; + case 21: + loadColorPack(colorPack, colorPack_RAINBOW); + Anim_ColorBreath(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 7000, 90); + break; + case 22: + loadColorPack(colorPack, colorPack_USA); + Anim_GradientRotate(AnimationLooping, ledSettings[0].leds, ledSettings[0].size, colorPack, 50); + break; + default: + ESP_LOGD(tag, "Loop default"); + break; + } + + AnimationLooping = false; + prevAnimEvent = AnimEvent; + ESP_LOGD(tag, "Going to Queue to Wait"); + } + } +} + + +void createFirePalette(CRGBPalette16& palette, CRGB color1, CRGB color2, CRGB color3) { + for (uint8_t i = 0; i < 16; i++) { + if (i < 3) palette[i] = CRGB::Black; + else if (i < 7) palette[i] = color1; + else if (i < 10) palette[i] = color2; + else if (i < 15) palette[i] = color3; + else palette[i] = CRGB::White; + } +} + +void loadColorPack(COLOR_PACK& dest, const COLOR_PACK& src) { + memcpy_P(&dest, &src, sizeof(COLOR_PACK)); +} diff --git a/src/Animations.cpp b/src/Animations.cpp new file mode 100644 index 0000000..0b36c2d --- /dev/null +++ b/src/Animations.cpp @@ -0,0 +1,571 @@ +#include "Animations.h" +#include +#include +#include +#include "ColorPalettes.h" +#include "esp_system.h" + + +typedef struct{ + float minSpeed; + float maxSpeed; + int rampCycles; +}SPEED_PROPERTIES; + + +void Animation_Init(void){ + // Create Palettes + +} + +// Animation Loop Template +void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function callback) { + if (!callback) { + ESP_LOGE("Animation_Loop", "Invalid callback function"); + return; + } + + loop_active_flag = true; + speed = constrain(speed, 0, MaxSpeed); + int loopDelay = max(MaxSpeed - speed, MinLoopDelay); + + ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications + + TickType_t xLastWakeTime; + TickType_t elapsedTicks; + TickType_t delayTicks; + int retVal = 0; + while(!retVal && loop_active_flag) { + xLastWakeTime = xTaskGetTickCount(); + + try { + retVal = callback(); // Call animation function + } catch (const std::exception& e) { + ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what()); + break; + } + + if(!loop_active_flag) return; + + // Calculate remaining time with overflow protection + elapsedTicks = xTaskGetTickCount() - xLastWakeTime; + delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0; + + // Check for termination request + if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } + } + + loop_active_flag = false; +} + +// Animation Loop Template +void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickType_t durationMs, std::function callback) { + loop_active_flag = true; + speed = constrain(speed, 0, MaxSpeed); + if (durationMs < 0) durationMs = 0; + + ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications + TickType_t startTicks = xTaskGetTickCount(); + TickType_t xLastWakeTime; + for(;;) { + xLastWakeTime = xTaskGetTickCount(); + + // Call animation function + int speedIncrease = callback(); // Call animation function + + if(!loop_active_flag) return; + + // Calculate combined speed with bounds protection + int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed); + + // Calculate delay with minimum protection + int loopDelay = MaxSpeed - totalSpeed; + loopDelay = max(loopDelay, MinLoopDelay); + + // Calculate remaining time with overflow protection + TickType_t elapsedTicks = xTaskGetTickCount() - xLastWakeTime; + TickType_t delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0; + + // Delay and Check for termination request + if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } + + // Check if duration reached and wait for loop_active_flag + if (durationMs >=0) { + TickType_t totalElapsed = xTaskGetTickCount() - startTicks; + + if (totalElapsed >= durationMs) { + while(loop_active_flag){ + if (ulTaskNotifyTake(pdTRUE, 50)) { + return; + } + } + break; + } + } + } + + loop_active_flag = false; +} + +// Animation Loop Template +void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function callback) { + loop_active_flag = true; + uint32_t loop_cycle_count = 0; + speed = constrain(speed, 0, MaxSpeed); + + ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications + TickType_t startTicks = xTaskGetTickCount(); + TickType_t xLastWakeTime; + TickType_t elapsedTicks; + TickType_t delayTicks; + for(;;) { + xLastWakeTime = xTaskGetTickCount(); + + int speedIncrease = callback(); // Call animation function + + if(!loop_active_flag) return; + + // Calculate combined speed with bounds protection + int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed); + + // Calculate delay with minimum protection + int loopDelay = MaxSpeed - totalSpeed; + loopDelay = max(loopDelay, MinLoopDelay); + + // Calculate remaining time with overflow protection + elapsedTicks = xTaskGetTickCount() - xLastWakeTime; + delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0; + + // Delay and Check for termination request + if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } + + // Check if cycles reached and wait for loop_active_flag + if (loop_cycle_count >= loop_cycles) { + while(loop_active_flag){ + if (ulTaskNotifyTake(pdTRUE, 50)) { + //loop_active_flag = false; + break; + } + } + break; + } + + loop_cycle_count++; + } + + loop_active_flag = false; +} + + +/******************************************************************************** + * + * Animations + * + *******************************************************************************/ + +void Anim_Rainbow(bool volatile& activeFlag, CRGB* leds, int size, int speed){ + // Initialize rainbow pattern once + fill_rainbow_circular(leds, size, 0); + + CRGB temp; + Animation_Loop(activeFlag, speed, [&]() -> int { + // Rotate pixels by 1 position + temp = leds[0]; // Save first pixel + memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB)); + leds[size-1] = temp; // Move first pixel to end + + FastLED.show(); + return 0; + }); +} + +// Fire parameters (adjustable) +const uint8_t FIRE_COOLING = 66; +const uint8_t FIRE_SPARKING = 62; +const uint8_t FIRE_brightness = 255; + +void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const CRGBPalette16& firePalette, int shift = 0) { + if (!leds || size <= 0) return; + + // Calculate half size for mirroring + const int halfSize = size / 2; + + // Create heat array for half the size + uint8_t* heat = new (std::nothrow) uint8_t[halfSize]; + if (!heat) return; + memset(heat, 0, halfSize * sizeof(uint8_t)); + + CRGB color; + uint8_t colorindex; + int pos1, pos2, y; + Animation_Loop(activeFlag, speed, [&]() -> int { + // Random cooling + for(int i = 0; i < halfSize; i++) { + heat[i] = qsub8(heat[i], random8(0, ((FIRE_COOLING * 10) / halfSize) + 2)); + } + + // Heat rises and diffuses + for(int k = halfSize - 1; k >= 2; k--) { + heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3; + } + + // Randomly ignite new sparks at bottom + if(random8() < FIRE_SPARKING) { + y = random8(7); + heat[y] = qadd8(heat[y], random8(160, 240)); + } + + // Map heat to colors with mirroring and shifting + for(int j = 0; j < halfSize; j++) { + colorindex = scale8(heat[j], 240); + color = ColorFromPalette(firePalette, colorindex, FIRE_brightness, LINEARBLEND); + + // Apply shift and wrap around + pos1 = (j + shift + size) % size; + pos2 = (size - 1 - j + shift + size) % size; + + leds[pos1] = color; + leds[pos2] = color; // Mirror + } + + FastLED.show(); + return 0; + }); + + delete[] heat; +} + + +void Anim_Color_Sectors(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, uint8_t numRepeats, int speed) { + // Validate inputs + if (size <= 0 || numRepeats <= 0 || colorPack.size <= 0) return; + + // Calculate sector size + int sectorSize = size / (colorPack.size * numRepeats); + if (sectorSize < 1) sectorSize = 1; + + // Initialize pattern + for (int repeat = 0; repeat < numRepeats; repeat++) { + for (int color = 0; color < colorPack.size; color++) { + int startIdx = (repeat * colorPack.size + color) * sectorSize; + int endIdx = startIdx + sectorSize; + if (endIdx > size) endIdx = size; + + // Fill sector with the specified color + fill_solid(&leds[startIdx], endIdx - startIdx, colorPack.col[color]); + } + } + + // Animate rotation + bool direction = true; // true = forward, false = backward + int loopCounter = 0; + CRGB temp; + Animation_Loop(activeFlag, speed, [&]() -> int { + + if (direction) { + temp = leds[0]; + memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB)); + leds[size-1] = temp; + } else { + temp = leds[size-1]; + memmove(&leds[1], &leds[0], (size-1) * sizeof(CRGB)); + leds[0] = temp; + } + + // Direction Switching Logic + loopCounter++; + if(loopCounter >= (size * CYCLES_PER_DIRECTION)){ + direction = !direction; + loopCounter = 0; + } + + FastLED.show(); + return 0; + }); +} + + + +/******************************************************************************** + * Comets Animation + *******************************************************************************/ +#define COMET_SIZE_FACTOR 0.2 +#define COMET_FADE_FACTOR1 64 /* longer tail */ +#define COMET_FADE_FACTOR2 192 /* shorter tail */ +#define COMET_FADE_FACTOR COMET_FADE_FACTOR2 +#define MAX_COMETS 16 // Maximum number of comets supported +/* +void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed, bool randomDecay, bool shorterTail, int cometMultiplier = 1) { + // Validate inputs + int numComets = colorPack.size; + if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0) return; + + // Calculate comet size + int cometSize = (size / (numComets * cometMultiplier)) * COMET_SIZE_FACTOR; + if (cometSize < 1) cometSize = 1; + + // Set fade factor + uint8_t fadeFactor = shorterTail ? COMET_FADE_FACTOR2 : COMET_FADE_FACTOR1; + + // Initialize comet positions to be equally spaced + std::unique_ptr cometPositions(new (std::nothrow) int[numComets * cometMultiplier]); + if (!cometPositions) return; + int totalComets = numComets * cometMultiplier; + int spacing = size / totalComets; + for (int i = 0; i < totalComets; i++) { + cometPositions[i] = i * spacing; + } + + // Animation loop + bool direction = true; // true = forward, false = backward + int loopCounter = 0; + try { + Animation_Loop(activeFlag, speed, [&]() -> int { + // Fade all LEDs + for (int i = 0; i < size; i++) { + if(!randomDecay) { + leds[i].fadeToBlackBy(fadeFactor); + } else if(random(10) > 5){ + leds[i].fadeToBlackBy(fadeFactor); + } + } + + // Move and draw comets + for (int i = 0; i < totalComets; i++) { + if (direction) { + cometPositions[i] = (cometPositions[i] + 1) % size; + } else { + cometPositions[i] = (cometPositions[i] - 1 + size) % size; + } + + // Draw comet with solid color + CRGB color = colorPack.col[i % colorPack.size]; + for (int j = 0; j < cometSize; j++) { + int pos = (cometPositions[i] - j + size) % size; + leds[pos] += color; + } + } + + loopCounter++; + if(loopCounter >= (size * CYCLES_PER_DIRECTION)){ + direction = !direction; + loopCounter = 0; + } + + FastLED.show(); + return 0; + }); + } catch (const std::exception& e) { + ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what()); + } catch (...) { + ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop"); + } + // No need to delete cometPositions as it is managed by std::unique_ptr +} +*/ + + +void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed, bool randomDecay, bool shorterTail, int cometMultiplier = 1) { + // Validate inputs + int numComets = colorPack.size; + int totalComets = numComets * cometMultiplier; + + if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0 || totalComets > MAX_COMETS) { + ESP_LOGE("Anim_Comets", "Invalid input parameters or too many comets (max: %d, requested: %d)", MAX_COMETS, totalComets); + return; + } + + // Calculate comet size + int cometSize = (size / totalComets) * COMET_SIZE_FACTOR; + if (cometSize < 1) cometSize = 1; + + // Set fade factor + uint8_t fadeFactor = shorterTail ? COMET_FADE_FACTOR2 : COMET_FADE_FACTOR1; + + // Initialize comet positions with fixed array + int cometPositions[MAX_COMETS] = {0}; + int spacing = size / totalComets; + for (int i = 0; i < totalComets; i++) { + cometPositions[i] = i * spacing; + } + + // Animation loop + bool direction = true; // true = forward, false = backward + int loopCounter = 0; + int pos; + CRGB color; + try { + Animation_Loop(activeFlag, speed, [&]() -> int { + // Fade all LEDs + for (int i = 0; i < size; i++) { + if (!randomDecay) { + leds[i].fadeToBlackBy(fadeFactor); + } else if (getRandomValue(10) > 5) { + leds[i].fadeToBlackBy(fadeFactor); + } + } + + // Move and draw comets + for (int i = 0; i < totalComets; i++) { + if (direction) { + cometPositions[i] = (cometPositions[i] + 1) % size; + } else { + cometPositions[i] = (cometPositions[i] - 1 + size) % size; + } + + // Draw comet with solid color + colorPack.col[i % colorPack.size]; + for (int j = 0; j < cometSize; j++) { + pos = (cometPositions[i] - j + size) % size; + leds[pos] += color; + } + } + + // Direction change + if (++loopCounter >= (size * CYCLES_PER_DIRECTION)) { + direction = !direction; + loopCounter = 0; + } + + FastLED.show(); + return 0; + }); + } catch (const std::exception& e) { + ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what()); + } catch (...) { + ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop"); + } +} + + +void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0) { + if (!leds || size <= 0 || totalDurationMs <= 0) return; + + const int halfSize = size / 2; + const float msPerLed = totalDurationMs / (float)halfSize; + unsigned long startTime = millis(); + + fill_solid(leds, size, baseCol); + + int prevLedsToLight = 0; + unsigned long currentTime; + unsigned long elapsedTime; + int ledsToLight, pos; + Animation_Loop(activeFlag, 90, [&]() -> int { + currentTime = millis(); + elapsedTime = currentTime - startTime; + + // Calculate how many LEDs should be lit based on elapsed time + ledsToLight = (elapsedTime / msPerLed); + if (ledsToLight > halfSize) ledsToLight = halfSize; + + // Fill LEDs up to current position + for (int i = 0; i < ledsToLight; i++) { + pos = (i + shift + size) % size; + leds[pos] = fillCol; + leds[size - 1 - pos] = fillCol; // Mirror + } + + // Update LEDs only when necessary + if(prevLedsToLight < ledsToLight){ + FastLED.show(); + } + prevLedsToLight = ledsToLight; + + // Return 1 when complete + return (ledsToLight >= halfSize) ? 1 : 0; + }); +} + + +#define MIN_BRIGHTNESS 2 +void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, uint32_t timeMs, int speed) { + if (!leds || size <= 0 || colors.size <= 0 || timeMs <= 0) return; + + uint8_t colorIndex = 0; + unsigned long startTime = millis(); + const uint32_t halfTime = timeMs / 2; + + uint8_t origBright = FastLED.getBrightness(); + FastLED.setBrightness(255); + + uint8_t brightness = MIN_BRIGHTNESS; + uint32_t elapsedTime; + CRGB scaledColor, correctedColor; + unsigned long currentTime; + Animation_Loop(activeFlag, speed, [&]() -> int { + // Calculate elapsed time in current breath cycle + currentTime = millis(); + elapsedTime = currentTime - startTime; + + // Calculate brightness using a linear approach + if (elapsedTime < halfTime) { + brightness = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten + } else { + brightness = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim + } + + // Scale the color directly + scaledColor = colors.col[colorIndex]; + scaledColor.nscale8(brightness * origBright / 255); + + // Correct the color scale for vision + correctedColor = scaledColor; + correctedColor.nscale8_video(brightness); + + // Fill all LEDs with scaled color + fill_solid(leds, size, correctedColor); + FastLED.show(); + + if (elapsedTime >= timeMs) { + colorIndex = (colorIndex + 1) % colors.size; + startTime = currentTime; + } + + return 0; + }); + + FastLED.setBrightness(origBright); +} + + +void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) { + if (!leds || size <= 0 || colors.size < 2) return; + + CRGB color1, color2; + + // Create initial gradient + int segmentLength = size / colors.size; + for(int i = 0; i < size; i++) { + // Determine which color segment we're in + int segment = i / segmentLength; + int nextSegment = (segment + 1) % colors.size; + + // Calculate blend amount within segment + uint8_t blendPos = map(i % segmentLength, 0, segmentLength - 1, 0, 255); + + // Create local copies for blending + color1 = colors.col[segment]; + color2 = colors.col[nextSegment]; + + // Set initial gradient + leds[i] = blend(color1, color2, blendPos); + } + + // Rotation animation loop + CRGB temp; + Animation_Loop(activeFlag, speed, [&]() -> int { + // Rotate one position + temp = leds[0]; + memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB)); + leds[size-1] = temp; + + FastLED.show(); + return 0; + }); +} + + +uint32_t getRandomValue(uint32_t maxValue) { + return esp_random() % maxValue; +} \ No newline at end of file diff --git a/src/AppUpgrade.cpp b/src/AppUpgrade.cpp new file mode 100644 index 0000000..433886d --- /dev/null +++ b/src/AppUpgrade.cpp @@ -0,0 +1,618 @@ +#include "AppUpgrade.h" +#include "esp_log.h" +#include +#include +#include +#include "global.h" +#include "jsonConstrain.h" +#include "BLE_UpdateService.h" + +static const char* TAG = "AppUpdater"; +TaskHandle_t Update_Task_Handle = NULL; +TaskHandle_t versionCheckTask_Handle = NULL; + +// Queue handle for firmware update messages +//QueueHandle_t updateMsgQueue = NULL; + +String updateUrl = ""; + +Version otaVersion; + + +AppUpdater::AppUpdater(fs::FS& fs, Version localVersion, const char* bucket, const char* manifestName, const char* appBin) + : localVersion(localVersion), bucketUrl(bucket), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE]) +{ + ESP_LOGI(TAG, "AppUpdater initialized with version %s", localVersion.toString().c_str()); +} + +void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) { + progressCb = callback; +} + +void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) { + status = newStatus; + if (progressCb) { + progressCb(status, percentage, message); + } +} + +bool AppUpdater::checkManifest() { + String url = String(bucketUrl) + manifestName; + ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str()); + + // Start the HTTP client and Send GET request for manifest + HTTPClient http; + http.begin(url); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode); + http.end(); + return false; + } + + // Read the response + String payload = http.getString(); + http.end(); + + // Parse JSON + DeserializationError error = deserializeJson(jsonManifest, payload); + ESP_LOGD(TAG, "Manifest deserialized"); + if (error) { + ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str()); + return false; + } + + // Check for files section + jsonFilesArray = jsonManifest["files"]; + if (jsonFilesArray.isNull()) { + ESP_LOGE(TAG, "No files section in manifest"); + return false; + }else{ + ESP_LOGD(TAG, "%d Files found", jsonFilesArray.size()); + } + + // Check for version section + JsonObject jsonVersion = jsonManifest["version"]; + ESP_LOGD(TAG, "Version section found"); + if (jsonVersion.isNull()) { + ESP_LOGE(TAG, "No version section in manifest"); + return false; + } + + // Get the remote version + byte major = jsonVersion["major"] | 0; + byte minor = jsonVersion["minor"] | 0; + byte patch = jsonVersion["patch"] | 0; + otaVersion = {major, minor, patch}; + + //Version localVersion; + //::sscanf(localVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch); + + // Check if an update is available + updateAvailable = false; + if (otaVersion < localVersion) { + ESP_LOGI(TAG, "No updates available"); + return false; + }else{ + updateAvailable = true; + ESP_LOGD(TAG, "Update available"); + } + + //ESP_LOGD(TAG, "Manifest content: %s", payload.c_str()); + + return true; +} + +bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) { + //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); + + // Construct full URL + String url = String(bucketUrl) + remotePath; + ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath); + + String localMd5 = getLocalMD5(localPath); + + if (localMd5.equals(expectedMd5)) { + ESP_LOGI(TAG, "File already up to date: %s", localPath); + updateProgress(UpdateStatus::FILE_SKIPPED, 100, localPath); + return true; + } + + // Start the download + HTTPClient http; + http.begin(url); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "Download failed: %d", httpCode); + updateProgress(UpdateStatus::ERROR, 0, "Download failed"); + http.end(); + return false; + } + + // Get the stream and content length + WiFiClient* stream = http.getStreamPtr(); + size_t contentLength = http.getSize(); + + // Verify and save the file + bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5); + http.end(); + if(!success){ + updateProgress( UpdateStatus::ERROR, 0, "MD5 verification failed"); + }else{ + updateProgress( UpdateStatus::FILE_SAVED, 100, localPath); + } + + return success; +} + +bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5) +{ + MD5Builder md5; + md5.begin(); + size_t totalRead = 0; + + // Create temporary filename + String tempPath = String(localPath) + ".tmp"; + + // Open temporary file for writing + File file = fileSystem.open(tempPath.c_str(), FILE_WRITE); + if (!file) { + ESP_LOGE(TAG, "Failed to open temporary file for writing"); + return false; + } + + //updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); + + // Single pass: Save file and calculate MD5 + while (totalRead < contentLength) { + size_t available = stream->available(); + if (available) { + size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE))); + + // Write to temp file and update MD5 + if (file.write(downloadBuffer.get(), readLen) != readLen) { + ESP_LOGE(TAG, "Failed to write to temporary file"); + + file.close(); + fileSystem.remove(tempPath.c_str()); + return false; + } + + md5.add(downloadBuffer.get(), readLen); + totalRead += readLen; + updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath); + } + yield(); + } + + file.close(); + md5.calculate(); + String calculatedMd5 = md5.toString(); + + // Verify MD5 hash + if (!calculatedMd5.equals(expectedMd5)) { + //ESP_LOGE(TAG, "MD5 mismatch for %s", localPath); + fileSystem.remove(tempPath.c_str()); + return false; + } + + // Replace original file with verified temp file + if (fileSystem.exists(localPath)) { + fileSystem.remove(localPath); + } + if (!fileSystem.rename(tempPath.c_str(), localPath)) { + ESP_LOGE(TAG, "Failed to rename temporary file"); + fileSystem.remove(tempPath.c_str()); + return false; + } + + return true; +} + +String AppUpdater::getLocalMD5(const char* filePath){ + File file = fileSystem.open(filePath, "r"); + if(!file){ + ESP_LOGE(TAG, "Error opening %s...", filePath); + return String(); + } + + MD5Builder md5Builder; + md5Builder.begin(); + size_t fileSize = file.size(); + size_t totalRead = 0; + size_t readLen = 0; + while (totalRead < fileSize) { + readLen = file.readBytes(reinterpret_cast(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE))); + md5Builder.add(downloadBuffer.get(), readLen); + totalRead += readLen; + } + + md5Builder.calculate(); + file.close(); + return md5Builder.toString(); +} + +bool AppUpdater::updateFilesArray() { + int successCount = 0; + int totalFiles = jsonFilesArray.size(); + ESP_LOGI(TAG, "Found %d files in manifest", totalFiles); + + // Iterate over each file entry in the manifest + for (JsonObject file : jsonFilesArray) { + const char* remotePath = file["remote"]; + const char* localPath = file["local"]; + const char* expectedMd5 = file["md5"]; + + // Skip invalid entries + if (!remotePath || !localPath || !expectedMd5) { + ESP_LOGE(TAG, "Invalid file entry in manifest"); + continue; + } + + // Attempt to update the file + if (updateFile(remotePath, localPath, expectedMd5)) { + successCount++; + } + } + + ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles); + return successCount == totalFiles; +} + +bool AppUpdater::updateApp() { + updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update"); + + // Check for firmware section in manifest + if (!jsonManifest["firmware"].is() || !jsonManifest["firmware"]["md5"].is()) { + ESP_LOGE(TAG, "Invalid firmware section in manifest"); + updateProgress(UpdateStatus::ERROR, 0, "Firmware: Invalid firmware section in manifest"); + return false; + } + + // Get the firmware MD5 hash and URL + const char* expectedMd5 = jsonManifest["firmware"]["md5"]; + String firmwareUrl = String(bucketUrl) + appName; + + // Download the firmware + HTTPClient http; + http.begin(firmwareUrl); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "Firmware download failed: %d", httpCode); + updateProgress(UpdateStatus::ERROR, 0, "Firmware: Firmware download failed"); + http.end(); + return false; + } + + // Check available space + size_t firmwareSize = http.getSize(); + if (!Update.begin(firmwareSize)) { + ESP_LOGE(TAG, "Firmware: Not enough space for update"); + updateProgress(UpdateStatus::ERROR, 0, "Firmware: Not enough space for update"); + http.end(); + return false; + } + + // Set up MD5 checking + MD5Builder md5; + md5.begin(); + + // Download and verify firmware + WiFiClient* stream = http.getStreamPtr(); + size_t remaining = firmwareSize; + while (remaining > 0) { + size_t chunk = std::min(remaining, size_t(BUFFER_SIZE)); + size_t read = stream->readBytes(downloadBuffer.get(), chunk); + + // Check for timeout + if (read == 0) { + ESP_LOGE(TAG, "Read timeout"); + + Update.abort(); + http.end(); + return false; + } + + // Update MD5 and write firmware + md5.add(downloadBuffer.get(), read); + if (Update.write(downloadBuffer.get(), read) != read) { + ESP_LOGE(TAG, "Write failed"); + Update.abort(); + http.end(); + return false; + } + + remaining -= read; + updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware"); + } + + // Verify MD5 + md5.calculate(); + String calculatedMd5 = md5.toString(); + if (!calculatedMd5.equals(expectedMd5)) { + ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str()); + updateProgress(UpdateStatus::MD5_FAILED, 0, "Firmware: MD5 mismatch"); + Update.abort(); + http.end(); + return false; + } + + // Finish update + if (!Update.end()) { + ESP_LOGE(TAG, "Update end failed"); + updateProgress(UpdateStatus::ERROR, 0, "Firmware: Update failed"); + http.end(); + return false; + } + + http.end(); + updateProgress(UpdateStatus::COMPLETE, 0, "Firmware: Complete"); + return true; +} + +bool AppUpdater::IsUpdateAvailable(){ + return updateAvailable; +} + + +AsyncEventSource* eventProgress = nullptr; +void startFirmwareUpdateTask(AsyncEventSource* evProg) { + eventProgress = evProg; + if(Update_Task_Handle) { + ESP_LOGW(TAG, "Firmware update task already running"); + return; + } + xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 1024*8, NULL, 1, &Update_Task_Handle); +} + +void firmwareUpdateTask(void* parameter) { + static const char* TAG = "UpdateTask"; + AppUpdater* updater = nullptr; + + try { + loadUpdateJson(); + + // Initialize updater + updater = new AppUpdater(LittleFS, localVersion, updateUrl.c_str(), "update.json", "firmware.bin"); + updater->setProgressCallback(updateProgress); + + ESP_LOGI(TAG, "Starting update check from: %s", updateUrl.c_str()); + + // Check and perform updates + if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); } + + if (updater->IsUpdateAvailable()) { + ESP_LOGI(TAG, "Update available, updating files..."); + + if (!updater->updateFilesArray()) { + throw std::runtime_error("Failed to update files"); + } + + ESP_LOGI(TAG, "Updating firmware..."); + if (!updater->updateApp()) { + throw std::runtime_error("Failed to update firmware"); + } + ESP_LOGI(TAG, "Update successful, restarting..."); + + sendUpdateMessage("Restarting ", true, 100); + vTaskDelay(2000); + + ESP.restart(); + + } + + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Update failed: %s", e.what()); + } + end: + delete updater; + Update_Task_Handle = NULL; + vTaskDelete(NULL); +} + + +void startVersionCheckTask() { + if(versionCheckTask_Handle != NULL) { + ESP_LOGW(TAG, "Version Check Tak already running"); + return; + } + xTaskCreate(versionCheckTask, "VersionCheckTask", 1024*8, NULL, 1, &versionCheckTask_Handle); +} + +void versionCheckTask(void* parameter){ + + if(updateUrl == ""){ + loadUpdateJson(); + } + + if(checkManifest(otaVersion) == false){ + ESP_LOGE(TAG, "Error checking manifest"); + } + + versionCheckTask_Handle = NULL; + vTaskDelete(NULL); +} + +void loadUpdateJson(void) { + try { + ESP_LOGD(TAG, "loadUpdateJaon function..."); + if(updateUrl == "") { + String updateJsonPath = "/system/update.json"; + + // Read and parse update.json + File file = LittleFS.open(updateJsonPath); + if (!file) { + throw std::runtime_error("Failed to open update.json"); + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { throw std::runtime_error("Failed to parse update.json"); } + + // Get update configuration + JsonObject jObj = doc.as(); + String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://storage.googleapis.com/boothifier/"); + String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/"); + updateUrl = baseUrl + folderName; + + ESP_LOGD(TAG, "updateUrl: %s", updateUrl.c_str()); + } + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Update failed: %s", e.what()); + } +} + +void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) { + + char buffer[128]; + const char* msg; + bool isComplete = false; + + switch (newStatus) { + case AppUpdater::UpdateStatus::IDLE: + snprintf(buffer, sizeof(buffer), "Update idle"); + msg = buffer; + break; + case AppUpdater::UpdateStatus::MESSAGE: + msg = message ? message : ""; + break; + case AppUpdater::UpdateStatus::DOWNLOADING: + snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", message, percentage); + msg = buffer; + break; + case AppUpdater::UpdateStatus::VERIFYING: + snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", message, percentage); + msg = buffer; + break; + case AppUpdater::UpdateStatus::FILE_SKIPPED: + snprintf(buffer, sizeof(buffer), "%s: Skipping file update, already up to date", message); + msg = buffer; + break; + case AppUpdater::UpdateStatus::FILE_SAVED: + snprintf(buffer, sizeof(buffer), "%s: File Saved", message); + msg = buffer; + break; + case AppUpdater::UpdateStatus::MD5_FAILED: + snprintf(buffer, sizeof(buffer), "%s: MD5 Verification Failed", message); + msg = buffer; + break; + case AppUpdater::UpdateStatus::COMPLETE: + snprintf(buffer, sizeof(buffer), "Firmware Update Complete!!!"); + msg = buffer; + isComplete = true; + break; + case AppUpdater::UpdateStatus::ERROR: + snprintf(buffer, sizeof(buffer), "Error!: %s", message); + msg = buffer; + break; + default: + snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus); + msg = buffer; + break; + } + + ESP_LOGI(TAG, "%s", msg); + sendUpdateMessage(msg, isComplete, percentage); +} + +void sendUpdateMessage(const char* message, bool complete, int progress = -1) { + if(eventProgress && eventProgress->count() > 0) { + JsonDocument jsonDoc; + jsonDoc["message"] = message; + jsonDoc["complete"] = complete; + jsonDoc["progress"] = progress; + String strMessage; + serializeJson(jsonDoc, strMessage); + eventProgress->send(strMessage.c_str(), "update", millis()); + } + else{ + ESP_LOGW(TAG, "No clients connected to event source"); + } + + bleUpgrade_send_message(message); +} + +bool checkManifest(Version& remoteVersion) { + const char* TAG = "manifestCheck"; + String url = updateUrl + "update.json"; + ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str()); + + // Start the HTTP client and send GET request for manifest + HTTPClient http; + http.begin(url); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode); + http.end(); + return false; + } + + // Read the response + String payload = http.getString(); + http.end(); + + //ESP_LOGD(TAG, "%s", payload.c_str()); + + // Parse JSON + JsonDocument jsonManifest; + DeserializationError error = deserializeJson(jsonManifest, payload); + if (error) { + ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str()); + return false; + } + + // Check for version section + JsonObject jsonVersion = jsonManifest["version"]; + + if (jsonVersion.isNull()) { + ESP_LOGE(TAG, "No version section in manifest"); + return false; + } + + // Get the remote version + byte major = jsonVersion["major"] | 0; + byte minor = jsonVersion["minor"] | 0; + byte patch = jsonVersion["patch"] | 0; + remoteVersion = {major, minor, patch}; + ESP_LOGI(TAG, "Remote version: %s", remoteVersion.toString().c_str()); + return true; +} + +/* +void setup() { + Serial.begin(115200); + + // Initialize WiFi connection first + // ... WiFi connection code ... + + // Initialize filesystem + if(!LittleFS.begin()) { + Serial.println("LittleFS Mount Failed"); + return; + } + + // Create updater instance with: + // - Current version: "1.0.0" + // - Update server URL: "https://my-update-server.com/" + // - Filesystem: LittleFS + AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS); + + // Set progress callback + updater.setProgressCallback([](int progress) { + Serial.printf("Update progress: %d%%\n", progress); + }); + + // Check and update firmware + if (updater.checkAndUpdate()) { + Serial.println("Update successful! Rebooting..."); + ESP.restart(); + } + + // Update specific files from manifest + int updatedFiles = updater.updateFilesFromManifest("test_update.json"); + Serial.printf("Updated %d files\n", updatedFiles); +} + +*/ \ No newline at end of file diff --git a/src/BLE_SP110E.cpp b/src/BLE_SP110E.cpp new file mode 100644 index 0000000..fba7765 --- /dev/null +++ b/src/BLE_SP110E.cpp @@ -0,0 +1,389 @@ +#include "BLE_SP110E.h" +//#include "NimBLEDevice.h" +#include "esp_log.h" +#include "WiFi.h" +#include "ATALights.h" + +static const char *tag = "BLE_SP110E"; + +#define BT_SERVICE "FFE0" +#define BT_SP110E_CHARACTERISTIC "FFE1" +NimBLECharacteristic *pSP110ECharacteristic = nullptr; + +#define BT_STICK_CHARACTERISTIC "FFE2" +NimBLECharacteristic *pStickCharacteristic = nullptr; + +NimBLEClient* pStickClient; +NimBLERemoteCharacteristic* pRemoteCharacteristic; +bool masterFound = false; +NimBLEAdvertisedDevice* myDevice; + +TaskHandle_t LightStick_Client_Task_Handle = NULL; + +//#define UPGRADE_SERVICE_UUID "abcdef01-2345-6789-1234-56789abcdef0" +//#define UPGRADE_CHARACTERISTIC_UUID "abcdef01-2345-6789-1234-56789abcdef1" + +//typedef enum {SM16703,TM1804,UCS1903,WS2811,WS2801,SK6812,LPD6803,LPD8806,APA102,APA105,DMX512,TM1914,TM1913,P9813,INK1003,P943S,P9411,P9413,TX1812,TX1813,GS8206,GS8208,SK9822,TM1814,SK6812_RGBW,P9414,PG412,IC_MODEL_COUNT } IC_MODELS; +//typedef enum {RGB,RBG,GRB,GBR,BRG,BGR,SEQUENCE_COUNT} SEQUENCES; + +typedef struct { + uint8_t checksum; + uint8_t enable; + uint8_t preset; + uint8_t speed; + uint8_t bright; + uint8_t ic_model; + uint8_t channel; // sequence + uint8_t count_msb; // 16bit number + uint8_t count_lsb; + uint8_t red; + uint8_t green; + uint8_t blue; + uint8_t white; +} INFO_PACK; + +INFO_PACK led_status = {}; +uint8_t led_num = 1; + +uint8_t calculateChecksum(const uint8_t bArr[]) { + return (uint8_t)((bArr[2]) | ((bArr[0] << 1) & 254 & 105) | bArr[1]); +} + +// Class for handling characteristic events +class SP110ECallbacks : public NimBLECharacteristicCallbacks { + void onWrite(NimBLECharacteristic *pCharacteristic) override { + std::string rawValue = pCharacteristic->getValue(); + const uint8_t* value = reinterpret_cast(rawValue.data()); + size_t length = rawValue.length(); + ESP_LOGI(tag, "Data received 0x%02X, 0x%02X, 0x%02X (length %zu):", value[0], value[1], value[2], length); + + sendToAllClients(value, length); + process_BLE_SP110E_Command(value, length, pCharacteristic); + /* + if (length >= 4) { + uint8_t command = value[3]; + ESP_LOGI(tag, "Command received: 0x%02X", command); + + uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer + + // Handle different commands + switch (command) { + case TURN_ON: + Lights_Set_ON(); + led_status.enable = 1; + //ESP_LOGI(tag, "Lights ON"); + break; + case TURN_OFF: + Lights_Set_OFF(); + led_status.enable = 0; + //ESP_LOGI(tag, "Lights OFF"); + break; + case SET_STATIC_COLOR: + led_status.red = value[1]; + led_status.green = value[2]; + led_status.blue = value[0]; + Lights_Set_Animation(SOLID_COLOR_INDEX, value[0], value[1], value[2]); + //ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue); + break; + case SET_BRIGHT: + led_status.bright = value[0]; + Lights_Set_Brightness(value[0]); + //ESP_LOGI(tag, "Bright set to %d", led_status.bright); + break; + case SET_WHITE: + led_status.white = value[0]; + Lights_Set_White(value[0]); + //ESP_LOGI(tag, "White set to %d", led_status.white); + break; + case SET_PRESET: + led_status.preset = value[0]; + Lights_Set_Animation(value[0], value[1], value[2], 0); + //ESP_LOGI(tag, "Animation set to %d", led_status.preset); + break; + case SET_SPEED: + led_status.speed = value[0]; + ESP_LOGI(tag, "Mode set to %d", led_status.speed); + break; + case GET_CHECK_DEVICE: // This prepends a checksum + led_status.checksum = calculateChecksum((uint8_t *)value); + pCharacteristic->setValue((uint8_t *)&led_status, sizeof(INFO_PACK)); + pCharacteristic->notify(); // Send the data immediately + ESP_LOGI(tag, "Check Device"); + break; + case GET_DEVICE_INFO: // No checksum + led_status.checksum = 0; + pCharacteristic->setValue(((uint8_t *)&led_status) + 1, sizeof(INFO_PACK) - 1); + pCharacteristic->notify(); // Send the data immediately + ESP_LOGI(tag, "Get Device Info"); + break; + case SET_IC_MODEL: + led_status.ic_model = value[0]; + ESP_LOGI(tag, "IC Model set to %d", led_status.ic_model); + break; + case SET_RGB_SEQUENCE: + led_status.channel = 0; + ESP_LOGI(tag, "Set RGB Sequence"); + break; + case SET_LED_NUM: + led_status.count_msb = 0; + led_status.count_lsb = 100; + ESP_LOGI(tag, "Set LED Num"); + break; + case SET_DEVICE_NAME: + ESP_LOGI(tag, "Set Device Name"); + break; + default: + ESP_LOGW(tag, "Unknown command: 0x%02X", command); + break; + } + } else { + ESP_LOGW(tag, "Data too short to process"); + } + */ + } +}; + +class LightStickCallbacks : public NimBLECharacteristicCallbacks { + void onRead(NimBLECharacteristic *pCharacteristic) override { + pCharacteristic->setValue("Hello..."); + pCharacteristic->notify(); + } +}; + + +void sendToAllClients(const uint8_t *data, size_t len) { + // Check if the characteristic is valid and has subscribed clients + if (pStickCharacteristic != nullptr) { + pStickCharacteristic->setValue(data, len); + // This call notifies all connected clients with the same message + pStickCharacteristic->notify(); + } +} + + +void process_BLE_SP110E_Command(const uint8_t* val, uint8_t len, NimBLECharacteristic* bleChar) { + + if (len >= 4) { + uint8_t command = val[3]; + ESP_LOGI(tag, "Command received: 0x%02X", command); + + uint8_t response[sizeof(INFO_PACK)]; // Use a single response buffer + + // Handle different commands + switch (command) { + case TURN_ON: + Lights_Set_ON(); + led_status.enable = 1; + //ESP_LOGI(tag, "Lights ON"); + break; + case TURN_OFF: + Lights_Set_OFF(); + led_status.enable = 0; + //ESP_LOGI(tag, "Lights OFF"); + break; + case SET_STATIC_COLOR: + led_status.red = val[1]; + led_status.green = val[2]; + led_status.blue = val[0]; + Lights_Set_Animation(SOLID_COLOR_INDEX, val[0], val[1], val[2]); + //ESP_LOGI(tag, "Color set to R:%d G:%d B:%d", led_status.red, led_status.green, led_status.blue); + break; + case SET_BRIGHT: + led_status.bright = val[0]; + Lights_Set_Brightness(val[0]); + //ESP_LOGI(tag, "Bright set to %d", led_status.bright); + break; + case SET_WHITE: + led_status.white = val[0]; + Lights_Set_White(val[0]); + //ESP_LOGI(tag, "White set to %d", led_status.white); + break; + case SET_PRESET: + led_status.preset = val[0]; + Lights_Set_Animation(val[0], val[1], val[2], 0); + //ESP_LOGI(tag, "Animation set to %d", led_status.preset); + break; + case SET_SPEED: + led_status.speed = val[0]; + ESP_LOGI(tag, "Mode set to %d", led_status.speed); + break; + case GET_CHECK_DEVICE: // This prepends a checksum + led_status.checksum = calculateChecksum(val); + if(bleChar != nullptr){ + bleChar->setValue((uint8_t *)&led_status, sizeof(INFO_PACK)); + bleChar->notify(); // Send the data immediately + } + ESP_LOGI(tag, "Check Device"); + break; + case GET_DEVICE_INFO: // No checksum + led_status.checksum = 0; + if(bleChar != nullptr){ + bleChar->setValue(((uint8_t *)&led_status) + 1, sizeof(INFO_PACK) - 1); + bleChar->notify(); // Send the data immediately + } + ESP_LOGI(tag, "Get Device Info"); + break; + case SET_IC_MODEL: + led_status.ic_model = val[0]; + ESP_LOGI(tag, "IC Model set to %d", led_status.ic_model); + break; + case SET_RGB_SEQUENCE: + led_status.channel = 0; + ESP_LOGI(tag, "Set RGB Sequence"); + break; + case SET_LED_NUM: + led_status.count_msb = 0; + led_status.count_lsb = 100; + ESP_LOGI(tag, "Set LED Num"); + break; + case SET_DEVICE_NAME: + ESP_LOGI(tag, "Set Device Name"); + break; + default: + ESP_LOGW(tag, "Unknown command: 0x%02X", command); + break; + } + } +} + + +void Init_BLE_SP110E(NimBLEServer* pServer) { + led_status.speed = 10; + led_status.bright = 50; + led_status.ic_model = 0; + led_status.count_lsb = 20; + + // Create BLE Service + NimBLEService *pService = pServer->createService( BT_SERVICE ); + + // Create FFE1 Characteristic with WRITE and NOTIFY properties + pSP110ECharacteristic = pService->createCharacteristic( + BT_SP110E_CHARACTERISTIC, + NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pSP110ECharacteristic->setCallbacks(new SP110ECallbacks()); + ESP_LOGI(tag, "Custom callback registered!"); + + /************* Light Stick Characteristic ***************/ + pStickCharacteristic = pService->createCharacteristic( + BT_STICK_CHARACTERISTIC, + NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pStickCharacteristic->setCallbacks(new LightStickCallbacks()); + ESP_LOGI(tag, "Custom callback registered!"); + + /****************************************************/ + + + // Start the Service + pService->start(); + + // Configure Advertising + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID( BT_SERVICE ); // Advertise the FFE0 service UUID + + const uint8_t manufacturerData[] = {0x00, 0x00, 0x38, 0x93, 0x0E, 0x12, 0xAA, 0x08}; // Example Manufacturer Data + pAdvertising->setManufacturerData(std::string((char *)manufacturerData, sizeof(manufacturerData))); + +} + + +// Callback for notifications from characteristic2 +class LightStickClientCallback : public NimBLEClientCallbacks { + void onNotify(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify) { + Serial.print("Notification received from characteristic2: "); + //Serial.write(pData, length); + //Serial.println(); + process_BLE_SP110E_Command(pData, length, nullptr); + } +}; + +void Init_BLE_LightStick_Client(){ + if(LightStick_Client_Task_Handle != NULL) { + ESP_LOGW(tag, "Light Stick Client Task already running"); + return; + } + xTaskCreate(BLE_LightStick_Client_Task, "VersionCheckTask", 1024*8, NULL, 1, &LightStick_Client_Task_Handle); +} + +// Task for the BLE LightStick client +// This task connects to the BLE server and waits for notifications +void BLE_LightStick_Client_Task(void *parameter) { + static const char *tag = "BLE_LightStick_Client_Task"; + ESP_LOGI(tag, "BLE LightStick Client Task started"); + + while (true) { + // Only try to connect if we're not already connected and a device is set. + if ((pStickClient == nullptr || !pStickClient->isConnected()) && myDevice != nullptr) { + // Create a new client instance if needed. + if (pStickClient == nullptr) { + pStickClient = NimBLEDevice::createClient(); + pStickClient->setClientCallbacks(new LightStickClientCallback()); + } + ESP_LOGI(tag, "Attempting to connect to the server"); + if (pStickClient->connect(myDevice)) { + ESP_LOGI(tag, "Connected to the server"); + + // Get the service. + NimBLERemoteService* pRemoteService = pStickClient->getService(BT_SERVICE); + if (pRemoteService == nullptr) { + ESP_LOGE(tag, "Failed to find service UUID: %s", BT_SERVICE); + pStickClient->disconnect(); + } else { + // Get the characteristic. + pRemoteCharacteristic = pRemoteService->getCharacteristic(BT_STICK_CHARACTERISTIC); + if (pRemoteCharacteristic != nullptr && pRemoteCharacteristic->canNotify()) { + pRemoteCharacteristic->subscribe(true, [](NimBLERemoteCharacteristic* pRemoteCharacteristic, + uint8_t* pData, size_t length, bool isNotify) { + Serial.print("Notification received: "); + Serial.write(pData, length); + Serial.println(); + process_BLE_SP110E_Command(pData, length, nullptr); + }); + } else { + ESP_LOGE(tag, "Failed to register for notifications"); + pStickClient->disconnect(); + } + } + } else { + ESP_LOGE(tag, "Failed to connect to the server"); + // Delete the client instance so that a new one is created next time. + if (pStickClient != nullptr) { + NimBLEDevice::deleteClient(pStickClient); + pStickClient = nullptr; + } + // Wait before retrying. + vTaskDelay(pdMS_TO_TICKS(5000)); + continue; + } + } + + // If connected, monitor the connection. + if (pStickClient != nullptr && pStickClient->isConnected()) { + // Here you could add additional connection health checks if needed. + vTaskDelay(pdMS_TO_TICKS(1000)); + } + else { + // Not connected. Wait a while before the next connection attempt. + vTaskDelay(pdMS_TO_TICKS(5000)); + } + } +} + + + +// Callback for BLE scan results +class MyAdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks { + void onResult(NimBLEAdvertisedDevice* advertisedDevice) { + if (advertisedDevice->getName() == "ESP32_BLE_Master") { // Replace with your server's name + NimBLEDevice::getScan()->stop(); + myDevice = advertisedDevice; + masterFound = true; + Serial.println("Device found!"); + } + } +}; \ No newline at end of file diff --git a/src/BLE_UpdateService.cpp b/src/BLE_UpdateService.cpp new file mode 100644 index 0000000..fe0381e --- /dev/null +++ b/src/BLE_UpdateService.cpp @@ -0,0 +1,151 @@ +#include "BLE_UpdateService.h" +#include "WiFi.h" +#include "my_wifi.h" +#include "global.h" +#include "AppUpgrade.h" +#include "AppVersion.h" + +static const char *tag = "BLE_UpdateService"; + +#define UPGRADE_SERVICE_UUID "abcdef01-2345-6789-1234-56789abcdef0" +#define UPGRADE_CHARACTERISTIC1_UUID "abcdef01-2345-6789-1234-56789abcdef1" +#define UPGRADE_CHARACTERISTIC2_UUID "abcdef02-2345-6789-1234-56789abcdef1" + +NimBLEService *pUpgradeService = nullptr; +NimBLECharacteristic *pUpgradeCharacteristic1 = nullptr; +NimBLECharacteristic *pUpgradeCharacteristic2 = nullptr; + +enum WIFI_STAT : byte { WIFI_DISCONNECTED=0, WIFI_BAD_CREDS=1, WIFI_NO_AP=2, WIFI_CONNECTED=3 }; + +struct updateStatus { + WIFI_STAT wifiStatus = WIFI_DISCONNECTED; + bool wifiOnline = false; + byte wifiIP[4] = {0, 0, 0, 0}; + byte currVersion[3] = {FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCH}; + byte newVersion[3] = {0, 0, 0}; +}updatePacket; + + + +// Class for handling characteristic events +class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks { + + void onWrite(NimBLECharacteristic *pCharacteristic) override { + std::string value = pCharacteristic->getValue(); + ESP_LOGD(tag, "Upgrade Char written with value: %s", value.c_str()); + + if (value.compare(0, 12, "wifi-connect") == 0) { // Update WiFi credentials + JsonDocument doc; + deserializeJson(doc, value.substr(13)); + JsonObject wifiJson = doc.as(); + String ssid = wifiJson["ssid"].as(); + String pass = wifiJson["pass"].as(); + ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str()); + + bool status = StartWifiConnectTask(ssid, pass); + if(status == true){ + updatePacket.wifiStatus = WIFI_DISCONNECTED; + updatePacket.wifiOnline = false; + updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0; + }else{ + ESP_LOGI(tag, "Failed to start WiFi connection task"); + } + } + else if (value.compare("version-check") == 0) { // Check if new version is available + ESP_LOGI(tag, "Version check command received: newVersion=%d.%d.%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch()); + if(updatePacket.newVersion[0] == 0){ + startVersionCheckTask(); // start the task and done + }else{ + ESP_LOGI(tag, "Version already checked"); + } + } + else if (value.compare("upgrade-start") == 0) { // Start OTA update + ESP_LOGI(tag, "Start OTA update command received"); + startFirmwareUpdateTask(nullptr); // start the task + } + else if (value.compare("rename-device") == 0) { // Start renaming device + ESP_LOGI(tag, "Start renane device command received"); + } + else { + ESP_LOGW(tag, "Unknown command received: %s", value.c_str()); + } + } + + void onRead(NimBLECharacteristic *pCharacteristic) override { + updatePacket.wifiOnline = InternetAvailable; + if(WiFi.status() == WL_CONNECTED){ + updatePacket.wifiStatus = WIFI_CONNECTED; + if(updatePacket.wifiIP[0] == 0){ + updatePacket.wifiIP[0] = WiFi.localIP()[0]; + updatePacket.wifiIP[1] = WiFi.localIP()[1]; + updatePacket.wifiIP[2] = WiFi.localIP()[2]; + updatePacket.wifiIP[3] = WiFi.localIP()[3]; + } + }else{ + updatePacket.wifiStatus = WIFI_DISCONNECTED; + if(updatePacket.wifiIP[0] > 0){ + updatePacket.wifiIP[0] = 0; + updatePacket.wifiIP[1] = 0; + updatePacket.wifiIP[2] = 0; + updatePacket.wifiIP[3] = 0; + } + } + + //update version + if(otaVersion.major() != 0){ + ESP_LOGI(tag, "Updated new version: major=%d, minor=%d, patch=%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch()); + updatePacket.newVersion[0] = otaVersion.major(); + updatePacket.newVersion[1] = otaVersion.minor(); + updatePacket.newVersion[2] = otaVersion.patch(); + } + + pCharacteristic->setValue(reinterpret_cast(&updatePacket), sizeof(updatePacket)); + ESP_LOGI(tag, "Upgrade Char read"); + } +}; + + +void bleUpgrade_send_message(String s){ + if(pUpgradeCharacteristic2){ + if (s != nullptr) { + pUpgradeCharacteristic2->setValue(s); + pUpgradeCharacteristic2->notify(); + } else { + ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message"); + } + } +} + + +void Init_UpgradeBLEService(NimBLEServer *pServer){ + + // Create Upgrade BLE Service + pUpgradeService= pServer->createService( UPGRADE_SERVICE_UUID ); + + pUpgradeCharacteristic1 = pUpgradeService->createCharacteristic( + UPGRADE_CHARACTERISTIC1_UUID, + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pUpgradeCharacteristic1->setCallbacks(new UpgradeChar_Callbacks()); + ESP_LOGI(tag, "Upgrade callback registered!"); + + + pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic( + UPGRADE_CHARACTERISTIC2_UUID, + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pUpgradeCharacteristic2->setCallbacks(new UpgradeChar_Callbacks()); + ESP_LOGI(tag, "Upgrade callback registered!"); + + pUpgradeService->start(); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID( UPGRADE_SERVICE_UUID ); // Advertise service UUID + + +} + diff --git a/src/BleServer.cpp b/src/BleServer.cpp new file mode 100644 index 0000000..358d148 --- /dev/null +++ b/src/BleServer.cpp @@ -0,0 +1,68 @@ +#include "BleServer.h" +#include "esp_log.h" +#include "BLE_SP110E.h" +#include "BLE_UpdateService.h" + +static const char* tag = "BleServer"; + + +// Class for handling server events +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer* pServer) override { + ESP_LOGI(tag, "Client connected"); + // Ensure advertising remains active even after a client connects + NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); + if (pAdvertising != nullptr) { + pAdvertising->start(); + } + } + + void onDisconnect(NimBLEServer* pServer) override { + ESP_LOGI(tag, "Client disconnected"); + // Restart advertising on disconnect to keep it active always + NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising(); + if (pAdvertising != nullptr) { + pAdvertising->start(); + } + } +}; + +void Init_BleServer( bool isSP110EActive, bool isUpgradeActive) { + + ESP_LOGI(tag, "Initializing BLE..."); + static bool isInitialized = false; + if (!isInitialized) { + NimBLEDevice::init("ATALIGHTS"); + //NimBLEDevice::setMTU(247); // Set preferred MTU size (max 247 for BLE) + } + + NimBLEServer *pServer = NimBLEDevice::createServer(); + if (pServer == nullptr) { + ESP_LOGE(tag, "Failed to create BLE server"); + return; + } + + // setup the Sever Callbacks + pServer->setCallbacks(new ServerCallbacks()); + + // Initialize BLE with SP110E Service + if(isSP110EActive){ + Init_BLE_SP110E(pServer); + } + + // Initialize BLE with Upgrade Service + if(isUpgradeActive){ + Init_UpgradeBLEService(pServer); + } + + // Start BLE advertising + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + if (pAdvertising != nullptr) { + if (!pAdvertising->start()) { + ESP_LOGE(tag, "Failed to start advertising"); + return; + } + } else { + ESP_LOGE(tag, "Failed to get advertising object"); + } +} \ No newline at end of file diff --git a/src/ColorPalettes.cpp b/src/ColorPalettes.cpp new file mode 100644 index 0000000..76fbecc --- /dev/null +++ b/src/ColorPalettes.cpp @@ -0,0 +1,12 @@ +#include "ColorPalettes.h" + +void Create_Red_Yellow_Violet_Palette(CRGBPalette16& customPalette) { + customPalette = CRGBPalette16( + CRGB::Red, CRGB::Yellow, CRGB::Violet, + CRGB::Red, CRGB::Yellow, CRGB::Violet, + CRGB::Red, CRGB::Yellow, CRGB::Violet, + CRGB::Red, CRGB::Yellow, CRGB::Violet, + CRGB::Red, CRGB::Yellow, CRGB::Violet, + CRGB::Red + ); +} diff --git a/src/JsonConstrain.cpp b/src/JsonConstrain.cpp new file mode 100644 index 0000000..b8094c2 --- /dev/null +++ b/src/JsonConstrain.cpp @@ -0,0 +1,122 @@ +#include "JsonConstrain.h" +#include +#include + +template +void logConstrainedValue(const char* tag, const char* key, T value); + +template <> +void logConstrainedValue(const char* tag, const char* key, int value) { + ESP_LOGD(tag, "Key [%s] value: %d", key, value); +} + +template <> +void logConstrainedValue(const char* tag, const char* key, float value) { + ESP_LOGD(tag, "Key [%s] value: %f", key, value); +} + +template +void logClamping(const char* tag, const char* key, T value, T limit, const char* condition); + +template <> +void logClamping(const char* tag, const char* key, int value, int limit, const char* condition) { + ESP_LOGW(tag, "Key [%s] value too %s (%d). Clamping to [%d].", key, condition, value, limit); +} + +template <> +void logClamping(const char* tag, const char* key, float value, float limit, const char* condition) { + ESP_LOGW(tag, "Key [%s] value too %s (%f). Clamping to [%f].", key, condition, value, limit); +} + + + + +template +T jsonConstrain(const char *tag, const JsonObject &jsonObject, const char *key, T min, T max, T def) { + // Check if the key exists and is not null + if (!jsonObject[key].is()) { + ESP_LOGW(tag, "Key [%s] not found or null. Using default value.", key); + return def; + } + + // Attempt to parse the value + JsonVariant jsonValue = jsonObject[key]; + + // Check if the value type matches the expected type + if (!jsonValue.is()) { + ESP_LOGW(tag, "Key [%s] type mismatch. Using default value.", key); + return def; + } + + // Get the value + T value = jsonValue.as(); + + // Constrain to range + if (value < min) { + logClamping(tag, key, value, min, "low"); + value = min; + } else if (value > max) { + logClamping(tag, key, value, max, "high"); + value = max; + } + + logConstrainedValue(tag, key, value); + return value; +} + + +const char* jsonConstrainChar(const char *tag, const JsonObject &jsonObject, const char *key, const char *def) { + if (!jsonObject[key].is()) { + ESP_LOGW(tag, "Key [%s] not found or null. Using default value.", key); + return strdup(def); + } + + String value = jsonObject[key].as(); + if (value.isEmpty()) { + ESP_LOGW(tag, "Key [%s] value is empty. Using default value.", key); + return strdup(def); + } + + ESP_LOGD(tag, "Key [%s] value: %s", key, value); + return strdup(value.c_str()); +} + + +String jsonConstrainString(const char *tag, const JsonObject &jsonObject, const char *key, String def) { + // Check if the key exists and is not null + if (!jsonObject[key].is()) { + ESP_LOGW(tag, "Key [%s] not found or null. Using default value [%s].", key, def.c_str()); + return def; + } + + // Extract the value as a String + String value = jsonObject[key].as(); + + // Check if the value is empty + if (value.isEmpty()) { + ESP_LOGW(tag, "Key [%s] value is empty. Using default value [%s].", key, def.c_str()); + return def; + } + + ESP_LOGD(tag, "Key [%s] value: %s", key, value.c_str()); + return value; +} + + +bool jsonConstrainBool(const char *tag, const JsonObject &jsonObject, const char *key, bool def) { + // Check if the key exists and is of type boolean + if (!jsonObject[key].is()) { + ESP_LOGW(tag, "Key [%s] not found or not a boolean. Using default value [%s].", key, def ? "true" : "false"); + return def; + } + + // Retrieve and return the value + bool value = jsonObject[key].as(); + ESP_LOGD(tag, "Key [%s] value: %s", key, value ? "true" : "false"); + return value; +} + + +// Explicit instantiations +template int jsonConstrain(const char *, const JsonObject &, const char *, int, int, int); +template float jsonConstrain(const char *, const JsonObject &, const char *, float, float, float); diff --git a/src/PWM_Output.cpp b/src/PWM_Output.cpp new file mode 100644 index 0000000..d071bfe --- /dev/null +++ b/src/PWM_Output.cpp @@ -0,0 +1,96 @@ +#include "PWM_Output.h" +#include + + +const char* tag = "pwmout"; + +const float binaryPow[17] = { + 0, 2, 4, 8, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 65535 +}; + +PWM_Output::PWM_Output(int8_t pin, uint8_t ch, uint8_t res, uint32_t freq, float maxDuty, bool visionCorrected){ + this->currDuty = 0; + this->ch = ch; + this->maxDuty = maxDuty; + this->visionCorrected = visionCorrected; + this->freq = freq; + setResolution(res); + pinMode(pin, OUTPUT); + + uint32_t actualFreq = ledcSetup(ch, freq, res); + if (actualFreq != freq) { + ESP_LOGE(tag, "pwmOut-> ch:%d ledcSetup failed! Requested freq: %d, Actual freq: %d", ch, freq, actualFreq); + return; + } + + ledcAttachPin(pin, ch); + setOutput(this->currDuty); +} + +void PWM_Output::setMaxDuty(float duty) { + // add any validation or constraints here + if(duty < 0) { + duty = 0.0; + } + else if(duty > 100.0) { + duty = 100.0; + } + maxDuty = duty; +} + +// Range is 0 to 100% +void PWM_Output::setOutput(float duty){ + // Clamp the duty cycle to the range [0.0, maxDuty] + if (duty < 0.0) { + duty = 0.0; + } else if (duty > maxDuty) { + duty = maxDuty; + } + + // calculate correct duty value + int outDutyVal; + if(this->visionCorrected){ + outDutyVal = linearizeOutput(duty); + } + else{ + outDutyVal = static_cast(duty * this->standardFactor); + } + + ledcWrite(this->ch, outDutyVal); + this->currOutVal = outDutyVal; + this->currDuty = duty; +} + + +void PWM_Output::setFreq(uint32_t fq){ + uint32_t newFreq; + if(this->freq != fq){ + newFreq = ledcChangeFrequency(this->ch, fq, this->res); + if(newFreq){ + this->freq = fq; + } + } +} + +void PWM_Output::setResolution(uint8_t res){ + this->res = res; + if(this->res < 4) this->res = 4; + if(this->res > 16) this->res = 16; + + this->standardFactor = binaryPow[res] * 0.01; + this->visionFactor = binaryPow[res] * 0.0001; + ESP_LOGD(tag, "factor=%f, vision=%f", this->standardFactor, this->visionFactor); +} + +int PWM_Output::linearizeOutput(float inp){ + if (inp < 0.0) { + inp = 0.0; + } else if (inp > 100.0) { + inp = 100.0; + } + return static_cast(inp * inp * this->visionFactor); +} + +int PWM_Output::getOutVal(void){ + return ledcRead(this->ch); +} \ No newline at end of file diff --git a/src/Ramp_Lights.cpp b/src/Ramp_Lights.cpp new file mode 100644 index 0000000..115a9f0 --- /dev/null +++ b/src/Ramp_Lights.cpp @@ -0,0 +1,62 @@ +#include "Ramp_Lights.h" +#include + +static const char* tag = "ramp"; + +// TODO add tickSkip to instanciation +#define TickDelayCount 5 + +RAMP_LIGHT::RAMP_LIGHT(OneButton* button, PWM_Output* pwmOutput, int min, int max, float step) + : button(button), pwmOutput(pwmOutput), min(min), max(max), step(step) { + + button->attachClick([](void* context) { static_cast(context)->singleClick(); }, this); + button->attachLongPressStart([](void* context) { static_cast(context)->longPressStart(); }, this); + button->attachLongPressStop([](void* context) { static_cast(context)->longPressStop(); }, this); + button->attachDuringLongPress([](void* context) { static_cast(context)->duringLongPress(); }, this); + + if(min < 0.0) min = 0.0; + if(max > 100.0) max = 100.0; + currentValue = min; + IsOn = false; + rampState = RampingUp; +} + +void RAMP_LIGHT::tick(){ + button->tick(); +} + +void RAMP_LIGHT::singleClick(){ + IsOn = ! IsOn; + if(IsOn){ + pwmOutput->setOutput(currentValue); + ESP_LOGD(tag, "Light On"); + }else{ + pwmOutput->setOutput(0.0); + ESP_LOGD(tag, "Light Off"); + } +} + +void RAMP_LIGHT::longPressStart(){ + tickCount = TickDelayCount; + ESP_LOGD(tag, "ramp long press start"); +} + +void RAMP_LIGHT::longPressStop(){ + if(IsOn){ + rampState = (rampState == RampingUp) ? RampingDown : RampingUp; + } + ESP_LOGD(tag, "ramp long press stop"); +} + +// Ramping Logic +void RAMP_LIGHT::duringLongPress(){ + if(IsOn){ + if (tickCount > 0 && --tickCount == 0) { + currentValue += (rampState == RampingUp) ? step : -step; + currentValue = constrain(currentValue, min, max); + pwmOutput->setOutput(currentValue); + ESP_LOGD(tag, "duty: %f, sent val: %d, actual val: %d", currentValue, pwmOutput->currOutVal, pwmOutput->getOutVal()); + tickCount = TickDelayCount; + } + } +} \ No newline at end of file diff --git a/src/common/HSVTable.cpp b/src/common/HSVTable.cpp new file mode 100644 index 0000000..a5643e7 --- /dev/null +++ b/src/common/HSVTable.cpp @@ -0,0 +1,175 @@ +#include "HSVTable.h" +#include +#include "LEDStrip.h" + + +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}; + +const uint8_t HSVlights[61] = +{0, 4, 8, 13, 17, 21, 25, 30, 34, 38, 42, 47, 51, 55, 59, 64, 68, 72, 76, +81, 85, 89, 93, 98, 102, 106, 110, 115, 119, 123, 127, 132, 136, 140, 144, +149, 153, 157, 161, 166, 170, 174, 178, 183, 187, 191, 195, 200, 204, 208, +212, 217, 221, 225, 229, 234, 238, 242, 246, 251, 255}; + +const uint8_t HSVpower[121] = +{0, 2, 4, 6, 8, 11, 13, 15, 17, 19, 21, 23, 25, 28, 30, 32, 34, 36, 38, 40, +42, 45, 47, 49, 51, 53, 55, 57, 59, 62, 64, 66, 68, 70, 72, 74, 76, 79, 81, +83, 85, 87, 89, 91, 93, 96, 98, 100, 102, 104, 106, 108, 110, 113, 115, 117, +119, 121, 123, 125, 127, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, +151, 153, 155, 157, 159, 161, 164, 166, 168, 170, 172, 174, 176, 178, 181, +183, 185, 187, 189, 191, 193, 195, 198, 200, 202, 204, 206, 208, 210, 212, +215, 217, 219, 221, 223, 225, 227, 229, 232, 234, 236, 238, 240, 242, 244, +246, 249, 251, 253, 255}; + +// the real HSV rainbow +rgbpixel_t trueHSV(int angle) +{ + byte red, green, blue; + + if (angle<60) {red = 255; green = HSVlights[angle]; blue = 0;} else + if (angle<120) {red = HSVlights[120-angle]; green = 255; blue = 0;} else + if (angle<180) {red = 0, green = 255; blue = HSVlights[angle-120];} else + if (angle<240) {red = 0, green = HSVlights[240-angle]; blue = 255;} else + if (angle<300) {red = HSVlights[angle-240], green = 0; blue = 255;} else + {red = 255, green = 0; blue = HSVlights[360-angle];} + + //return {red, green, blue}; + return {red, green, blue}; +} + +// the 'power-conscious' HSV rainbow +rgbpixel_t powerHSV(int angle) +{ + byte red, green, blue; + if (angle<120) {red = HSVpower[120-angle]; green = HSVpower[angle]; blue = 0;} else + if (angle<240) {red = 0; green = HSVpower[240-angle]; blue = HSVpower[angle-120];} else + {red = HSVpower[angle-240]; green = 0; blue = HSVpower[360-angle];} + + return {red, green, blue}; +} + +// sine wave rainbow +rgbpixel_t sineHSV(int angle) +{ + return {lights[(angle+120)%360], lights[angle], lights[(angle+240)%360]}; +} + + +// hsv out: 0-360, input rgb 0-255 +float RGBToHSV(rgbpixel_t rgb) { + float delta; // min; + float h = 0, v; //, s; + + uint8_t min_ = std::min(std::min(rgb.red, rgb.grn), rgb.blu); + v = std::max(std::max(rgb.red, rgb.grn), rgb.blu); + delta = v - min_; + + if (rgb.red == v){ + h = (rgb.grn - rgb.blu) / delta; + }else if (rgb.grn == v){ + h = 2 + (rgb.blu - rgb.red) / delta; + }else if (rgb.blu == v){ + h = 4 + (rgb.red - rgb.grn) / delta; + } + + h *= 60; + + if(h < 0.0){ h = h + 360;} + if(h > 359.99){ h = 359.99;} + return isnan(h) ? 0.0 : h; +} + + +rgbpixel_t HSVToRGB(float H) { + float r = 0.0, g = 0.0, b = 0.0; + float S = 1.0; + float V = 1.0; + + if (H < 0) { H += 360; } + if (H > 359) { H -= 360; } + + if (S == 0.0) { + r = V; g = V; b = V; + }else { + int i; + float f, p, q, t; + + if (H == 360){ H = 0; } + else { H = H / 60; } + + i = (int)trunc(H); + f = H - i; + + p = V * (1.0 - S); + q = V * (1.0 - (S * f)); + t = V * (1.0 - (S * (1.0 - f))); + + switch (i) { + case 0: + r = V; g = t; b = p; break; + case 1: + r = q; g = V; b = p; break; + case 2: + r = p; g = V; b = t; break; + case 3: + r = p; g = q; b = V; break; + case 4: + r = t; g = p; b = V; break; + default: + r = V; g = p; b = q; break; + } + } + + rgbpixel_t pix; + pix.red = (uint8_t)(r * 255); + pix.grn = (uint8_t)(g * 255); + pix.blu = (uint8_t)(b * 255); + + return pix; +} + diff --git a/src/common/HSVTable.h b/src/common/HSVTable.h new file mode 100644 index 0000000..f88957c --- /dev/null +++ b/src/common/HSVTable.h @@ -0,0 +1,23 @@ +#ifndef HSVTABLE_H +#define HSVTABLE_H + +#include "LEDStrip.h" + + +rgbpixel_t trueHSV(int angle); + +rgbpixel_t powerHSV(int angle); + +rgbpixel_t sineHSV(int angle); + +rgbpixel_t HSVToRGB(float H); + +// Returne a 0-360 float +float RGBToHSV(rgbpixel_t rgb); + +// Input 0-360 float + + + + +#endif \ No newline at end of file diff --git a/src/common/LEDStrip.cpp b/src/common/LEDStrip.cpp new file mode 100644 index 0000000..8d7d2f0 --- /dev/null +++ b/src/common/LEDStrip.cpp @@ -0,0 +1,473 @@ +#include "LEDStrip.h" +#include "driver/i2s.h" +#include +#include "HSVTable.h" + +static const char* tag = "LEDStrip"; + +LEDSTRIP::LEDSTRIP(int port, int size, int pin, LED_ORDER ledOrder, int shift, int offset) +{ + this->port = port; + this->size = size; + this->shift = shift; + this->offset = offset; + this->effSize = this->size - this->offset; + this->ledOrder = ledOrder; + + // create pixel array and buffer array + pixels = new rgbpixel_t[size]; + out_buffer_size = size * PIXEL_SIZE; + out_buffer = new uint8_t[out_buffer_size + RESET_BUFFER_SIZE]; + reset_buffer = &out_buffer[out_buffer_size + 1]; // pointer to reset buffer section + active_out_buffer = &out_buffer[0] + (offset * PIXEL_SIZE); // start of the first active + + // initialize buffer + memset(pixels, 0, this->size * sizeof(rgbpixel_t)); + memset(out_buffer, 0, out_buffer_size + RESET_BUFFER_SIZE); + + dma_frame_size = I2S_TX_DMA_BUFF_SIZE; + if((size * PIXEL_SIZE) < I2S_TX_DMA_BUFF_SIZE) { + dma_frame_size = size * PIXEL_SIZE; + } + + // Although the buffer can be smaller than total buffer size it will require more cpu interrupts + // So this provides for the entire size rounded up to the closes buffer chunk (I2S_TX_DMA_BUFF_SIZE) + int dma_buff_count = 1 + ((out_buffer_size + RESET_BUFFER_SIZE + 2) / I2S_TX_DMA_BUFF_SIZE); + + // install driver + i2s_driver_config_t i2s_config = { + .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX), + .sample_rate = SAMPLE_RATE, + .bits_per_sample = I2S_BITS_PER_SAMPLE_8BIT, + .channel_format = i2s_channel_fmt_t(I2S_CHANNEL_FMT_RIGHT_LEFT), + .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, // high interrupt priority, + //.intr_alloc_flags = ESP_INTR_FLAG_LOWMED, // high interrupt priority, + .dma_buf_count = dma_buff_count, // + .dma_buf_len = I2S_TX_DMA_BUFF_SIZE, + .use_apll = true + }; // possibly needed for accuracy + + //i2sEventQueue = xQueueCreate(8, sizeof(i2s_event_t)); + esp_err_t err = i2s_driver_install((i2s_port_t)port, &i2s_config, 8, &i2sEventQueue); + + if(err){ + if(err == ESP_ERR_INVALID_ARG) {ESP_LOGE(tag, "I2S driver error: Parameter error");} + else if(err == ESP_ERR_NO_MEM) {ESP_LOGE(tag, "I2S driver error: Out of Memory");} + else if(err == ESP_ERR_INVALID_STATE) {ESP_LOGE(tag, "I2S driver error: Port is in use");} + } + else{ + i2s_pin_config_t pin_config = {.bck_io_num = I2S_PIN_NO_CHANGE, + .ws_io_num = I2S_PIN_NO_CHANGE, + .data_out_num = pin, + .data_in_num = I2S_PIN_NO_CHANGE }; + + ESP_LOGD(tag, "I2S port pin configured...."); + err = i2s_set_pin((i2s_port_t)port, &pin_config); + if(err){ + if(err == ESP_ERR_INVALID_ARG) {ESP_LOGE(tag, "I2S driver error: Parameter error");} + else if(err == ESP_FAIL) {ESP_LOGE(tag, "I2S driver error: IO error");} + } + else{ + ESP_LOGD(tag, "I2S pin config success!"); + } + } + +} + + +#if(HIGH_RES_BIT_RATE == 1) +static const uint8_t bitpatterns[2] = {0b11000000, 0b11111100}; +void LEDSTRIP::updateBuff(void) { + uint8_t* buff = active_out_buffer; + for (uint16_t i = 0; i < this->effSize; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 7 & 0x01]; + *buff++ = bitpatterns[red >> 6 & 0x01]; + *buff++ = bitpatterns[red >> 5 & 0x01]; + *buff++ = bitpatterns[red >> 4 & 0x01]; + *buff++ = bitpatterns[red >> 3 & 0x01]; + *buff++ = bitpatterns[red >> 2 & 0x01]; + *buff++ = bitpatterns[red >> 1 & 0x01]; + *buff++ = bitpatterns[red & 0x01]; + + *buff++ = bitpatterns[grn >> 7 & 0x01]; + *buff++ = bitpatterns[grn >> 6 & 0x01]; + *buff++ = bitpatterns[grn >> 5 & 0x01]; + *buff++ = bitpatterns[grn >> 4 & 0x01]; + *buff++ = bitpatterns[grn >> 3 & 0x01]; + *buff++ = bitpatterns[grn >> 2 & 0x01]; + *buff++ = bitpatterns[grn >> 1 & 0x01]; + *buff++ = bitpatterns[grn & 0x01]; + + *buff++ = bitpatterns[blu >> 7 & 0x01]; + *buff++ = bitpatterns[blu >> 6 & 0x01]; + *buff++ = bitpatterns[blu >> 5 & 0x01]; + *buff++ = bitpatterns[blu >> 4 & 0x01]; + *buff++ = bitpatterns[blu >> 3 & 0x01]; + *buff++ = bitpatterns[blu >> 2 & 0x01]; + *buff++ = bitpatterns[blu >> 1 & 0x01]; + *buff++ = bitpatterns[blu & 0x01]; + } + + *buff++ = 0; +} +#else +static const uint16_t bitpatterns[4] = {0x88, 0x8e, 0xe8, 0xee}; +void LEDSTRIP::updateBuff(void) +{ + uint8_t* buff = active_out_buffer; + + switch(this->ledOrder){ + case ORDER_RGB: + for (uint16_t i = 0; i < this->effSize; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + } + break; + case ORDER_RBG: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + } + break; + case ORDER_GRB: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + } + break; + case ORDER_GBR: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + } + break; + case ORDER_BRG: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + } + break; + default: //ORDER_BGR: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + } + break; + } +} +#endif + +void LEDSTRIP::show(bool update) +{ + if(update){ updateBuff(); }; + + //i2s_stop((i2s_port_t)this->port); + i2s_start((i2s_port_t)this->port); + i2s_zero_dma_buffer((i2s_port_t)this->port); + i2s_write_data(out_buffer, out_buffer_size + RESET_BUFFER_SIZE); + + /* + i2s_event_t i2s_event; + while(1){ + if(xQueueReceive(i2sEventQueue, &i2s_event, 100)){ + if(i2s_event.type == I2S_EVENT_TX_DONE) { break;} + }else{ + break; + } + } + */ +} + +void LEDSTRIP::setPowerDiv(uint8_t div){ + if(div < 0){div = 0;} + if(div > 4){div = 4;} + powerDiv = div; +} + +// Function to write data to the I2S buffer +void LEDSTRIP::i2s_write_data(const void *data, size_t len) +{ + size_t bytesWritten = 0; + + while (bytesWritten < len) { + size_t bytesToWrite = len - bytesWritten; + + size_t written = 0; + esp_err_t err = i2s_write(I2S_NUM_0, (const char*)data + bytesWritten, bytesToWrite, &written, portMAX_DELAY); + if (err != ESP_OK) { + // Handle error + ESP_LOGE(tag, "i2s send error: %d", err); + break; + } + + bytesWritten += written; + } +} + +inline int LEDSTRIP::calcIndex(int index) +{ + //int x = (index + shift) % effSize; + //if(x < 0) { // convert nex index to positive range + // x += effSize; + //} + //return x + offset; + int x = (index + shift) % effSize; + x = (x < 0) ? (x + effSize) : x; + return (x + offset) % effSize; +} + +void LEDSTRIP::setPixel(int index, rgbpixel_t& col) +{ + pixels[calcIndex(index)] = col; +} + +void LEDSTRIP::setPixel(int index, const rgbpixel_t col) +{ + pixels[calcIndex(index)] = col; +} + +/* +void LEDSTRIP::setPixel(int index, rgbpixel_t col, uint8_t scale) +{ + uint16_t n = scale + 1; + uint8_t r = (col.red * n) >> 8; + uint8_t g = (col.grn * n) >> 8; + uint8_t b = (col.blu * n) >> 8; + pixels[calcIndex(index)] = {r, g, b}; +} +*/ + +void LEDSTRIP::setPixelRaw(int index, rgbpixel_t& col){ + pixels[index] = col; +} + +void LEDSTRIP::setPixelMirrored(int index, rgbpixel_t col) +{ + pixels[calcIndex(index)] = col; + pixels[calcIndex(-index-1)] = col; +} + +rgbpixel_t LEDSTRIP::getPixel(int index) +{ + return pixels[calcIndex(index)]; +} + +void LEDSTRIP::rotatePixels(LED_DIR dir) +{ + // store the shifted out pixels + rgbpixel_t tPix; + if(dir == DIR_FWD){ + tPix = pixels[size -1]; + for(int i=size-1; i > offset; i--){ + pixels[i] = pixels[i-1]; + } + pixels[offset] = tPix; + }else{ + tPix = pixels[offset]; + for(int i=offset; i < size-1; i++){ + pixels[i] = pixels[i+1]; + } + pixels[size-1] = tPix; + } +} + +void LEDSTRIP::shiftPixels(int numPixels, LED_DIR dir) +{ + // Calculate the shift direction and absolute shift count + int shiftDir = (dir == DIR_FWD) ? 1 : -1; + int absShiftCount = (numPixels % this->effSize) * shiftDir; + + // Shift the pixels by swapping them in place using circular buffering + for (int i = 0; i < this->effSize; i++) { + rgbpixel_t temp = pixels[i]; + int j = i; + + while (true) { + int k = j + absShiftCount; + + if (k < 0) { + k += this->effSize; + } else if (k >= this->effSize) { + k -= this->effSize; + } + + if (k == i) { + break; + } + + pixels[j] = pixels[k]; + j = k; + } + + pixels[j] = temp; + + // Check if we have completed a cycle + if (j == i + absShiftCount) { + break; + } + } +} + +void scalePixel(rgbpixel_t& pix, uint8_t _scale) +{ + uint16_t n = _scale + 1; + pix.red = (pix.red * n) >> 8; + pix.grn = (pix.grn * n) >> 8; + pix.blu = (pix.blu * n) >> 8; +} + +void linearizePixel(rgbpixel_t& pix){ + pix.red = ((int)pix.red * (int)pix.red ) >> 8; + pix.grn = ((int)pix.grn * (int)pix.grn ) >> 8; + pix.blu = ((int)pix.blu * (int)pix.blu ) >> 8; +} + + +void PixelFadeToBlack(rgbpixel_t& pix, uint8_t fadeValue) { + pix.red = (pix.red <= 8) ? 0 : pix.red - ((pix.red * fadeValue)>>8); + pix.grn = (pix.grn <= 8) ? 0 : pix.grn - ((pix.grn * fadeValue)>>8); + pix.blu = (pix.blu <= 8) ? 0 : pix.blu - ((pix.blu * fadeValue)>>8); +} + +void LEDSTRIP::zeroPixels(void) +{ + for(int i=0; i < this->size; i++){ + pixels[i] = {0,0,0}; + } +} + +void LEDSTRIP::fill(rgbpixel_t color, int startIndex, int count) +{ + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) { + pixels[calcIndex(i)] = color; + } +} + +// a factor of 0=no changer, factor=255 newCol replaces 100% +void LEDSTRIP::transitionPixel(int index, rgbpixel_t& newCol, uint8_t factor) +{ + int i = calcIndex(index); + int f = factor + 1; + pixels[i].red = (pixels[i].red * (256 - f) + newCol.red * f) >> 8; + pixels[i].grn = (pixels[i].grn * (256 - f) + newCol.grn * f) >> 8; + pixels[i].blu = (pixels[i].blu * (256 - f) + newCol.blu * f) >> 8; +} + +LED_ORDER getRGBOrder(const char* order){ + if(strcmp(order, "rgb")==0){ return ORDER_RGB; } + if(strcmp(order, "rbg")==0){ return ORDER_RBG; } + if(strcmp(order, "grb")==0){ return ORDER_GRB; } + if(strcmp(order, "gbr")==0){ return ORDER_GBR; } + if(strcmp(order, "brg")==0){ return ORDER_BRG; } + if(strcmp(order, "bgr")==0){ return ORDER_BGR; } + else{ return ORDER_GRB; } +} \ No newline at end of file diff --git a/src/common/LEDStrip.h b/src/common/LEDStrip.h new file mode 100644 index 0000000..6c18f0a --- /dev/null +++ b/src/common/LEDStrip.h @@ -0,0 +1,117 @@ +#ifndef LEDSTRIP_H +#define LEDSTRIP_H + +//#include +#include "LEDStrip.h" +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_intr_alloc.h" + +#define HIGH_RES_BIT_RATE 0 + +#if(HIGH_RES_BIT_RATE == 1) + #define PIXEL_SIZE (8*3) + #define SAMPLE_RATE (360000) // 1.6us cycle + #define RESET_BUFFER_SIZE (100 * PIXEL_SIZE) // led latching timeout 300us mininum (50us older chips) +#else + // This can be set to 1 (normally 50) as long at the update freq isn't fast enough to interfere with the required reset length time + // This reduces memory requirements + #define RESET_PIXELS 1 + #define PIXEL_SIZE (4*3)// each colour takes 4 bytes and 3 colors per pixel + #define SAMPLE_RATE (160000) // 1.6us cycle + #define RESET_BUFFER_SIZE (RESET_PIXELS * PIXEL_SIZE) // led latching timeout 4 or more pixel lengths +#endif + +#define I2S_TX_DMA_BUFF_SIZE 128 // smaller buffer means it will start quicker + +typedef struct{ + uint8_t red; + uint8_t grn; + uint8_t blu; +} rgbpixel_t; + +enum LED_DIR{DIR_REV, DIR_FWD}; + +enum LED_ORDER{ORDER_RGB, ORDER_RBG, ORDER_GRB, ORDER_GBR, ORDER_BRG, ORDER_BGR}; + +LED_ORDER getRGBOrder(const char* order); + +void PixelFadeToBlack(rgbpixel_t& pix, uint8_t fadeValue); + +void scalePixel(rgbpixel_t& pix, uint8_t _scale); +void linearizePixel(rgbpixel_t& pix); +//rgbpixel_t scalePixel(rgbpixel_t pix, uint8_t _scale); + +//void i2sDmaInterruptHandler(void *arg); + +class LEDSTRIP { + public: + SemaphoreHandle_t i2sSemaphore; + int effSize; + int size; + int shift; + int offset; + int powerDiv = 0; + LED_ORDER ledOrder; + rgbpixel_t* pixels; + QueueHandle_t i2sEventQueue; + + LEDSTRIP(int port, int size, int pin, LED_ORDER ledOrder, int shift=0, int offset=0); + // process buffer before sending + void updateBuff(void); + // Send data to dma and I2S port + void show(bool update=true); + + void setPixel(int index, rgbpixel_t& col); + void setPixel(int index, const rgbpixel_t col); + void setPixel(int index, rgbpixel_t& col, uint8_t scale=255); + //void setPixelTrueHue(int index, int hueAngle); + //void setPixelPowerHue(int index, int hueAngle); + //void setPixelSineHue(int index, int hueAngle); + void setPixelRaw(int index, rgbpixel_t& col); + + void setPixelMirrored(int index, rgbpixel_t col); + rgbpixel_t getPixel(int index); + + //void scale(rgbpixel_t& pix, uint8_t _scale); + void fill(rgbpixel_t color, int startIndex, int count); + void fade(rgbpixel_t& col); + void zeroPixels(void); + //void setMirrored(void); + + // mirror pixels about the shift position + void rotatePixels(LED_DIR dir); + void shiftPixels(int numPixels, LED_DIR dir); + void transitionPixel(int index, rgbpixel_t& newCol, uint8_t factor); + //rgbpixel_t hueToRGB(uint8_t hue, uint8_t sat, uint8_t val); + + void setPowerDiv(uint8_t div); + + + private: + static int instanceCount; //number in class instances + + int port; + + + //intr_handle_t i2sInterruptHandle; + + int dma_frame_size; + // dma output buffer + uint8_t* out_buffer; + int out_buffer_size; + // pointer to actual active part of buffer + uint8_t* active_out_buffer; + // index position calculation + uint8_t* reset_buffer; + void i2s_write_data(const void *data, size_t size); + int calcIndex(int ind); + +}; + +//void IRAM_ATTR i2s0DmaTransmitComplete(void* arg); +//void IRAM_ATTR i2s1DmaTransmitComplete(void* arg); + +#endif diff --git a/src/common/color_tools.cpp b/src/common/color_tools.cpp new file mode 100644 index 0000000..3dc53d0 --- /dev/null +++ b/src/common/color_tools.cpp @@ -0,0 +1,69 @@ +#include "color_tools.h" +#include + +#define MAX_HUE 360.0 + + +/************************* CLASS - HUE PALLET DISPENSER ************************/ + +HUE_PALLET_DISPENSER::HUE_PALLET_DISPENSER(void){ + Initialize(0, 360, 6); +} + +void HUE_PALLET_DISPENSER::Initialize(float hue, float range, int colSteps){ + this->range = range; + this->hueSteps = colSteps; + + this->startHue = hue - this->range / 2; + if(this->startHue < 0.0){ + this->startHue += MAX_HUE; + }else if(this->startHue > MAX_HUE){ + this->startHue -= MAX_HUE; + } + + this->hueIndex = 0; + + if(this->hueSteps <= 1){ + this->hueIncrement = 0.0; + this->currHue = hue; + }else{ + this->hueIncrement = this->range / (this->hueSteps - 1); + } +} + + +int HUE_PALLET_DISPENSER::GetNextPalletHue(void){ + if(this->hueSteps > 1){ + this->currHue = this->startHue + this->hueIndex * this->hueIncrement; + if(this->currHue < 0){ + this->currHue += MAX_HUE; + }else if(this->currHue > MAX_HUE){ + this->currHue -= MAX_HUE; + } + + // TODO Remove later + //rgbpixel_t p = HUEtoRGB(huePallet.currHue); + //Log.traceln(" index: %d, hue= %F, col: %d, %d, %d", huePallet.hueIndex, huePallet.currHue, p.red, p.grn, p.blu); + + this->hueIndex = ++this->hueIndex % this->hueSteps; + return round(this->currHue); + }else{ + return round(this->currHue); + } +} + +float HUE_PALLET_DISPENSER::PeekNextPalletHue(int hueOffset){ + float tempHue = this->startHue + (this->hueIndex + hueOffset) * this->hueIncrement; + + if(tempHue < 0){ + tempHue += MAX_HUE; + }else if(tempHue > MAX_HUE){ + tempHue -= MAX_HUE; + } + + return tempHue; +} + +void HUE_PALLET_DISPENSER::SetHueIndex(int hueIndex){ + this->currHue = hueIndex; +} diff --git a/src/common/color_tools.h b/src/common/color_tools.h new file mode 100644 index 0000000..176ef2c --- /dev/null +++ b/src/common/color_tools.h @@ -0,0 +1,38 @@ +#ifndef _COLOR_TOOLS_H +#define _COLOR_TOOLS_H + + +class HUE_PALLET_DISPENSER { + public: + float range = 1.0; + float hueIncrement = 1.0; + float currHue = 1.0; + int hueIndex = 0; + int hueSteps = 1; + float startHue = 0; + + //HUE_PALLET_DISPENSER(float hue1, float hue2, int colSteps); + HUE_PALLET_DISPENSER(void); + void Initialize(float hue, float range, int colSteps); + int GetNextPalletHue(void); + float PeekNextPalletHue(int offset); + void SetHueIndex(int hueIndex); + private: +}; + +enum SPEED_PROFILE {SPEED_SIMPLE, SPEED_SQUARE, SPEED_LIN_ACCEL}; + +class SPEED_DISPENSER{ + public: + + SPEED_DISPENSER(void); + + void Initialize(int min, int max, int steps, SPEED_PROFILE prof); + int GetNextSpeedInterval(void); +}; + + + + + +#endif \ No newline at end of file diff --git a/src/common/fileSystem.cpp b/src/common/fileSystem.cpp new file mode 100644 index 0000000..685cb6b --- /dev/null +++ b/src/common/fileSystem.cpp @@ -0,0 +1,153 @@ +#include "fileSystem.h" +#include +#include + +static const char *tag = "fs"; + +#define LogEnabled true + +void Init_File_System(void){ + if (!LittleFS.begin()){ + ESP_LOGE(tag, "An Error occured while mounting LittleFS"); + return; + } + + // Get all information of your LITTLEFS + ESP_LOGD(tag, "File system info."); + ESP_LOGD(tag, "Total: %d, Used: %d, Free:%d", LittleFS.totalBytes(), LittleFS.usedBytes(), LittleFS.totalBytes() - LittleFS.usedBytes()); + + // Open dir folder + File dir = LittleFS.open("/"); + + // Cycle all the content + printAllFiles(); +} + +void getAllDirectories(String (&directoryList)[MAX_DIRECTORIES], int &count){ + File root = LittleFS.open("/"); + if (!root || !root.isDirectory()){ + ESP_LOGE("FileSystem", "Failed to open root directory or root is not a directory"); + return; + } + + directoryList[0] = "/"; + count = 1; + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()){ + if (count < 10){ // Ensure we don't overflow the array + directoryList[count] = '/' + String(file.name()); + count++; + }else{ + ESP_LOGW("FileSystem", "Directory list array is full"); + break; + } + } + file = root.openNextFile(); + } + + root.close(); +} + +void printFilesInDirectories(const String directoryList[], int count){ + for (int i = 0; i < count; i++){ + ESP_LOGD(tag, "Dir: %s", directoryList[i].c_str()); + + File dir = LittleFS.open(directoryList[i]); + if (!dir || !dir.isDirectory()){ + ESP_LOGE(tag, "Failed to open directory: %s", directoryList[i].c_str()); + continue; + } + + File file = dir.openNextFile(); + while (file){ + if (!file.isDirectory()){ + ESP_LOGD(tag, " File: %s", file.name()); + } + file = dir.openNextFile(); + } + + dir.close(); + } +} + +void printAllFiles(void){ + String directories[MAX_DIRECTORIES]; + int dirCount = 0; + + getAllDirectories(directories, dirCount); + + ESP_LOGD(tag, "File System Listing:"); + printFilesInDirectories(directories, dirCount); +} + +/*****************************************************************************************/ + +/* +void getAllDirectoriesRecursive(const String& path, String directoryList[], int& count, int maxSize) { + File dir = LittleFS.open(path); + if (!dir || !dir.isDirectory()) { + ESP_LOGE("FileSystem", "Failed to open directory: %s", path.c_str()); + return; + } + + File file = dir.openNextFile(); + while (file) { + if (file.isDirectory()) { + if (count < maxSize) { // Ensure we don't overflow the array + String dirName = String(file.name()); + if (!dirName.startsWith("/")) { + dirName = "/" + dirName; + } + directoryList[count] = dirName; + count++; + getAllDirectoriesRecursive(dirName, directoryList, count, maxSize); // Recursive call for subdirectories + } else { + ESP_LOGW("FileSystem", "Directory list array is full"); + break; + } + } + file = dir.openNextFile(); + } + + dir.close(); +} + +void getAllDirectories(String directoryList[], int& count, int maxSize) { + count = 0; + getAllDirectoriesRecursive("/", directoryList, count, maxSize); +} + +void printFilesInDirectories(const String directoryList[], int count) { + for (int i = 0; i < count; i++) { + ESP_LOGD(tag, "Dir: %s", directoryList[i].c_str()); + + File dir = LittleFS.open(directoryList[i]); + if (!dir || !dir.isDirectory()) { + ESP_LOGE(tag, "Failed to open directory: %s", directoryList[i].c_str()); + continue; + } + + File file = dir.openNextFile(); + while (file) { + if (!file.isDirectory()) { + ESP_LOGD(tag, " File: %s", file.name()); + } + file = dir.openNextFile(); + } + + dir.close(); + } +} + +void printAllFiles(int maxDirs) { + String directories[maxDirs]; + int dirCount = 0; + + getAllDirectories(directories, dirCount, maxDirs); + + ESP_LOGD(tag, "File System Listing:"); + printFilesInDirectories(directories, dirCount); +} +*/ \ No newline at end of file diff --git a/src/common/fileSystem.h b/src/common/fileSystem.h new file mode 100644 index 0000000..8c97d04 --- /dev/null +++ b/src/common/fileSystem.h @@ -0,0 +1,28 @@ +#ifndef _FILESYSTEM_H +#define _FILESYSTEM_H + +#define SPIFFS LITTLEFS +#include + +const int MAX_DIRECTORIES = 16; + +void Init_File_System(void); + +void readTestFile(void); + +void writeFilesToSerial(void); + +//void getAllDirectoriesRecursive(const String& path, String directoryList[], int& count, int maxSize); + +//void getAllDirectories(String directoryList[], int& count, int maxSize); +void getAllDirectories(String (&directoryList)[MAX_DIRECTORIES], int& count); + +void printFilesInDirectories(const String directoryList[], int count); + +//void printAllFiles(int maxDirs); +void printAllFiles(void); + + + + +#endif \ No newline at end of file diff --git a/src/common/luma-stiks.cpp b/src/common/luma-stiks.cpp new file mode 100644 index 0000000..3ad80e3 --- /dev/null +++ b/src/common/luma-stiks.cpp @@ -0,0 +1,249 @@ + +#include "luma-stiks.h" +#include "WiFi.h" + +static const char* tag = "stiks"; + +int PeerCount = 0; +PEER_ADDR peerList[LUMA_MAX_PEERS]; + +/* + -30 is absolute min, -90 is max +*/ +// TODO default rssi +LUMA_NODE* Luma_ScanAPMaching(String startName, float minRssi, int &countFound){ + // Initialize count + countFound = 0; + + // Start WiFi scan + int n = WiFi.scanNetworks(); + if (n == 0) { + Serial.println("No networks found"); + return nullptr; + } else { + // Dynamically allocate memory for lumaStik array + LUMA_NODE *node = new LUMA_NODE[n]; + + Serial.println("Networks found:"); + for (int i = 0; i < n; ++i) { + //if (WiFi.SSID(i).startsWith(nameFilter) && WiFi.RSSI(i) >= minRssi) { + if ( WiFi.SSID(i).startsWith(startName) ) { + // Update lumaStik array with details + node[countFound].ssid = WiFi.SSID(i); + memccpy(&node[countFound].peer.peer_addr, WiFi.BSSID(i), 6, sizeof(uint8_t)); + countFound++; + + ESP_LOGD(tag, "SSID: %s, MAC: %s, RSSi: %d", WiFi.SSID(i), WiFi.BSSIDstr(i), WiFi.RSSI(i)); + } + vTaskDelay(10); // Short delay for responsiveness + } + + WiFi.scanDelete(); + // If count is 0, free the allocated memory and return nullptr + if (countFound == 0) { + delete[] node; + return nullptr; + } + + return node; + } +} + +//bool Luma_Find_Node(const char* master_ssid, PEER_ADDR &addr, int &ch){ +bool Luma_Find_Node(const char* master_ssid, esp_now_peer_info_t &node){ + int n = WiFi.scanNetworks(); + bool ret = false; + + for(int i = 0; i < n; ++i){ + if(strcmp(WiFi.SSID(i).c_str(), master_ssid) == 0){ + uint8_t* mac = WiFi.BSSID(i); + memcpy(node.peer_addr, mac, 6); + node.channel = WiFi.channel(i); + String macStr = Luma_MacToStr(mac); + ESP_LOGD(tag, "Found: %s, mac: %s, ch: %d", WiFi.SSID(i).c_str(), macStr.c_str(), node.channel); + ret = true; + break; + } + } + + WiFi.scanDelete(); + return ret; +} + + +#define MAC_LENGTH 6 +int Luma_ConvertMACStringToMACAddress(const char* mChar, uint8_t* macAddr) { + // Check for null pointer or empty string + if (mChar == nullptr || strlen(mChar) < 17) { + return 1; + } + + uint8_t cnt = 0; + + for (uint8_t i = 0, j = 0; i < 17; i += 3, j++) { + int highNibble, lowNibble; + + highNibble = mChar[i]; + lowNibble = mChar[i + 1]; + + // Convert hexadecimal digit to decimal + highNibble = isdigit(highNibble) ? highNibble - '0' : toupper(highNibble) - 'A' + 10; + lowNibble = isdigit(lowNibble) ? lowNibble - '0' : toupper(lowNibble) - 'A' + 10; + + // Construct the byte and save it + macAddr[j] = (highNibble << 4) | lowNibble; + } + + return 0; +} + +esp_err_t Luma_Send_Packet(PEER_ADDR &peerAddr, LUMA_PACKET &packet){ + ESP_LOGD(tag, "Sending to Mac: %s", Luma_MacToStr(peerAddr).c_str()); + + esp_err_t result = esp_now_send(peerAddr, (const uint8_t*) &packet, sizeof(LUMA_PACKET)); + + if (result == ESP_OK) { + Serial.println("Success"); + } else if (result == ESP_ERR_ESPNOW_NOT_INIT) { + Serial.println("ESPNOW not Init."); + } else if (result == ESP_ERR_ESPNOW_ARG) { + Serial.println("Invalid Argument"); + } else if (result == ESP_ERR_ESPNOW_INTERNAL) { + Serial.println("Internal Error"); + } else if (result == ESP_ERR_ESPNOW_NO_MEM) { + Serial.println("ESP_ERR_ESPNOW_NO_MEM"); + } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) { + Serial.println("Peer not found."); + } else { + Serial.println("Not sure what happened"); + } + + return result; +} + + +void Luma_SendToAll_Packet(LUMA_PACKET &packet){ + for(int i = 0; i < PeerCount; i++){ + Luma_Send_Packet(peerList[i], packet); + } +} + + +esp_err_t Luma_Node_Check(PEER_ADDR &addr){ + uint8_t a = 100; + return esp_now_send(addr, (const uint8_t*) &a, 1); +} + + +LUMA_NODE* Luma_Scan_For_Stations(String nameFilter, float minRssi, int &countFound) { + countFound = 0; + + // Start WiFi scan + int n = WiFi.scanNetworks(); + if (n == 0) { + Serial.println("No networks found"); + return nullptr; + } + else { + // Dynamically allocate memory for lumaStik array + LUMA_NODE *node = new LUMA_NODE[n]; + + Serial.println("Networks found:"); + for (int i = 0; i < n; ++i) { + //if (WiFi.SSID(i).startsWith(nameFilter) && WiFi.RSSI(i) >= minRssi) { + if ( WiFi.SSID(i).startsWith(nameFilter) ) { + // Update lumaStik array with details + node[countFound].ssid = WiFi.SSID(i); + memccpy(&node[countFound].peer.peer_addr, WiFi.BSSID(i), 6, sizeof(uint8_t)); + countFound++; + + ESP_LOGD(tag, "SSID: %s, MAC: %s, RSSi: %d", WiFi.SSID(i), WiFi.BSSIDstr(i), WiFi.RSSI(i)); + } + vTaskDelay(10); // Short delay for responsiveness + } + + // If count is 0, free the allocated memory and return nullptr + if (countFound == 0) { + delete[] node; + return nullptr; + } + + return node; + } +} + +esp_err_t Luma_Register_with_Master( esp_now_peer_info_t &masterNode, const char* nodeName){ + LUMA_PACKET packet; + packet.type = LPT_REG; + LUMA_REGISTER_PACKET *reg = (LUMA_REGISTER_PACKET *)packet.data; + strncpy(reg->ssid, nodeName, strlen(reg->ssid) + 1); + return Luma_Send_Packet(masterNode.peer_addr, packet); +} + +String Luma_MacToStr(uint8_t* mac){ + String macStr; + + for (int i = 0; i < 6; ++i) { + // Convert each byte to a hexadecimal string + // and append it to the String object + String section = String(mac[i], HEX); + if (section.length() == 1) { + section = "0" + section; + } + macStr += section; + + // Add colon between sections, but not after the last one + if (i < 5) { + macStr += ":"; + } + } + + // Convert the String to uppercase + macStr.toUpperCase(); + + return macStr; +} + +int Luma_PeerCount(){ + return PeerCount; +} + +bool Luma_Add_Peer(esp_now_peer_info_t &node) { + // check if the peer exists + bool exists = esp_now_is_peer_exist(node.peer_addr); + if ( exists) { + // Slave already paired. + ESP_LOGD(tag, "Already Paired"); + return true; + } else { + // Slave not paired, attempt pair + esp_err_t addStatus = esp_now_add_peer(&node); + if (addStatus == ESP_OK) { + // Pair success + memcpy(&peerList[PeerCount][0], node.peer_addr, 6); + PeerCount++; + ESP_LOGD(tag, "Pair success"); + return true; + } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) { + // How did we get so far!! + ESP_LOGD(tag, "ESPNOW Not Init"); + return false; + } else if (addStatus == ESP_ERR_ESPNOW_ARG) { + ESP_LOGD(tag, "Invalid Argument"); + return false; + } else if (addStatus == ESP_ERR_ESPNOW_FULL) { + ESP_LOGD(tag, "Peer list full"); + return false; + } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) { + ESP_LOGD(tag, "Out of memory"); + return false; + } else if (addStatus == ESP_ERR_ESPNOW_EXIST) { + ESP_LOGD(tag, "Peer Exists"); + return true; + } else { + ESP_LOGD(tag, "Not sure what happened"); + return false; + } + } + +} \ No newline at end of file diff --git a/src/common/luma-stiks.h b/src/common/luma-stiks.h new file mode 100644 index 0000000..f635950 --- /dev/null +++ b/src/common/luma-stiks.h @@ -0,0 +1,63 @@ +#ifndef _LUMA_STIKS_H +#define _LUMA_STIKS_H + +#include +#include + +#define LUMA_MAX_PEERS 10 + +typedef uint8_t PEER_ADDR[6]; + +typedef struct { + bool enabled; + String ssid; + esp_now_peer_info_t peer; +}LUMA_NODE; + +enum LUMA_PACKET_TYPE { LPT_RAW, LPT_REG, LPT_ANIM}; + +typedef struct{ + uint8_t data[64]; +}LUMA_RAW_PACKET; + +typedef struct{ + char ssid[24]; + uint8_t mac[6]; + uint8_t ch; +}LUMA_REGISTER_PACKET; + +typedef struct{ + LUMA_PACKET_TYPE type; + uint8_t data[64]; +}LUMA_PACKET; + + +LUMA_NODE* Luma_ScanAPMaching(String startName, float minRssi, int &count); + +bool Luma_Find_Node(const char* master_ssid, esp_now_peer_info_t &node); + +int Luma_ConvertMACStringToMACAddress(const char* mChar, uint8_t* macAddr); + +esp_err_t Luma_Node_Check(PEER_ADDR &addr); + +esp_err_t Luma_Send_Packet(PEER_ADDR &peerAddr, LUMA_PACKET &packet); + +void Luma_SendToAll_Packet(LUMA_PACKET &packet); + + +//void Luma_Send_Node_Array(LUMA_NODE *node[], int count, LUMA_PACKET &packet); + +// TODO default RSSI +LUMA_NODE* Luma_Scan_For_Stations(String nameFilter, float minRssi, int &countFound); + +esp_err_t Luma_Register_with_Master( esp_now_peer_info_t &masterNode, const char* nodeName); + +String Luma_MacToStr(uint8_t* mac); + +bool Luma_Add_Peer(esp_now_peer_info_t &node); + +int Luma_PeerCount(); + + + +#endif \ No newline at end of file diff --git a/src/common/neo_colors.h b/src/common/neo_colors.h new file mode 100644 index 0000000..41e4cbd --- /dev/null +++ b/src/common/neo_colors.h @@ -0,0 +1,138 @@ +#ifndef NEO_COLORS_H +#define NEO_COLORS_H + +#include "LEDStrip.h" + +/* +rgbpixel_t pallet_rainbow[1]; +rgbpixel_t pallet_white[1]; +rgbpixel_t pallet_USA[1]; +rgbpixel_t pallet_halloween[1]; +rgbpixel_t pallet_christmas[1]; +rgbpixel_t pallet_autumn[1]; +rgbpixel_t pallet_summer[1]; +rgbpixel_t pallet_neon[1]; +*/ + + + +#define USE_CORRECTED_COLORS 0 + +#define col_black color_pallet[0] +#define col_white color_pallet[1] +#define col_red color_pallet[2] +#define col_green color_pallet[3] +#define col_blue color_pallet[4] +#define col_orange color_pallet[5] +#define col_yellow color_pallet[6] +#define col_cyan color_pallet[7] +#define col_magenta color_pallet[8] +#define col_purple color_pallet[9] +#define col_pink color_pallet[10] +#define col_teal color_pallet[11] +#define col_lime color_pallet[12] +#define col_indigo color_pallet[13] +#define col_maroon color_pallet[14] +#define col_navy color_pallet[15] +#define col_olive color_pallet[16] +#define col_beige color_pallet[17] +#define col_brown color_pallet[18] +#define col_coral color_pallet[19] +#define col_gold color_pallet[20] +#define col_gray color_pallet[21] +#define col_ivory color_pallet[22] +#define col_khaki color_pallet[23] +#define col_lavender color_pallet[24] +#define col_peach color_pallet[25] +#define col_periwinkle color_pallet[26] +#define col_salmon color_pallet[27] +#define col_sienna color_pallet[28] +#define col_silver color_pallet[29] +#define col_tan color_pallet[30] +#define col_turquoise color_pallet[31] +#define col_violet color_pallet[32] + + +#if USE_CORRECTED_COLORS == 0 + +const rgbpixel_t color_pallet[] = { + {0 , 0 , 0 }, // col_black + {255, 255, 255}, // col_white + {255, 0 , 0 }, // col_red + {0 , 255, 0 }, // col_green + {0 , 0 , 255}, // col_blue + {255, 165, 0 }, // col_orange + {255, 255, 0 }, // col_yellow + {0 , 255, 255}, // col_cyan + {255, 0 , 255}, // col_magenta + {128, 0 , 128}, // col_purple + {255, 192, 203}, // col_pink + {0 , 128, 128}, // col_teal + {0 , 255, 0 }, // col_lime + {75 , 0 , 130}, // col_indigo + {128, 0 , 0 }, // col_maroon + {0 , 0 , 128}, // col_navy + {128, 128, 0 }, // col_olive + {245, 245, 220}, // col_beige + {165, 42 , 42 }, // col_brown + {255, 127, 80 }, // col_coral + {255, 215, 0 }, // col_gold + {128, 128, 128}, // col_gray + {255, 255, 240}, // col_ivory + {240, 230, 140}, // col_khaki + {230, 230, 250}, // col_lavender + {255, 218, 185}, // col_peach + {204, 204, 255}, // col_periwinkle + {250, 128, 114}, // col_salmon + {160, 82 , 45}, // col_sienna + {192, 192, 192}, // col_silver + {210, 180, 140}, // col_tan + {64 , 224, 208}, // col_turquoise + {238, 130, 238} // col_violet +}; + +#else + +const rgbpixel_t color_pallet[] = +{ + {0 , 0 , 0 }, // col_black + {255, 255, 255}, // col_white + {255, 0 , 0 }, // col_red + {0 , 255, 0 }, // col_green + {0 , 0 , 255}, // col_blue + + {255, 128, 0 }, // col_orange + {255, 255, 0 }, // col_yellow + {0 , 255, 255}, // col_cyan + {255, 0 , 255}, // col_magenta + {170, 0 , 255}, // col_purple + {255, 170, 255}, // col_pink + {0 , 128, 128}, // col_teal + {128, 255, 0 }, // col_lime + {85 , 0 , 255}, // col_indigo + {128, 0 , 0 }, // col_maroon + {0 , 0 , 128}, // col_navy + {128, 128, 0 }, // col_olive + {255, 230, 204}, // col_beige + {153, 51 , 0 }, // col_brown + {255, 102, 102}, // col_coral + {204, 153, 0 }, // col_gold + {128, 128, 128}, // col_gray + {255, 255, 204}, // col_ivory + {204, 204, 0 }, // col_khaki + {204, 153, 255}, // col_lavender + {255, 204, 153}, // col_peach + {153, 153, 255}, // col_periwinkle + {255, 153, 102}, // col_salmon + {153, 76 , 0 }, // col_sienna + {204, 204, 204}, // col_silver + {204, 153, 102}, // col_tan + {0 , 204, 204}, // col_turquoise + {204, 0 , 255} // col_violet +}; +#endif + + + + +#endif diff --git a/src/global.cpp b/src/global.cpp new file mode 100644 index 0000000..6e0ed45 --- /dev/null +++ b/src/global.cpp @@ -0,0 +1,369 @@ +#include "global.h" +#include +#include +#include +#include +#include "JsonConstrain.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +static const char* tag = "global"; +const char* appName; + +SYS_TASK_HANDLES TaskList; +enum COMM_MODE commMode = COMM_WIFI_AP_BLE; +CHIP_INFO chipInfo; +Version localVersion = {FIRMWARE_VERSION_MAJOR, FIRMWARE_VERSION_MINOR, FIRMWARE_VERSION_PATCH}; + +void get_chip_mac(char* macStr, size_t size) { + uint64_t chipMAC = ESP.getEfuseMac(); + uint8_t macByte[6]; + + // Extract the MAC address bytes + for (int i = 0; i < 6; ++i) { + macByte[i] = (chipMAC >> (8 * (5 - i))) & 0xFF; + } + + // Format the MAC address string + snprintf(macStr, size, "%02X:%02X:%02X:%02X:%02X:%02X", + macByte[0], macByte[1], macByte[2], + macByte[3], macByte[4], macByte[5]); +} + +void print_ram_info(void) { + char s[64]; + snprintf(s, sizeof(s), "Total RAM: %u, Free: %u, Used: %u", + ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getHeapSize() - ESP.getFreeHeap()); + ESP_LOGD(tag, "RAM: %s", s); +} + +void print_chip_info(void) { + char macStr[64]; + get_chip_mac(macStr, sizeof(macStr)); + ESP_LOGD(tag, "Chip MAC: %s", macStr); + + print_ram_info(); +} + + +void printTaskInfo(TaskStatus_t taskStatus) { + uint32_t ulTotalRunTime = taskStatus.ulRunTimeCounter; + uint32_t ulStatsAsPercentage = (ulTotalRunTime * 100) / ulTotalRunTime; // Total runtime equals 100% + + ESP_LOGI(tag, "TaskInfo: %s\t%u\t%u\t%u\t%u%%", + taskStatus.pcTaskName, + taskStatus.uxCurrentPriority, + taskStatus.xCoreID, + configMINIMAL_STACK_SIZE - taskStatus.usStackHighWaterMark, + ulStatsAsPercentage); +} + +void printTaskCPUUsage(TaskHandle_t xTask) { + char buffer[512]; + vTaskGetRunTimeStats(buffer); + + // Parse the buffer to find the information for xTask + char* taskInfo = strstr(buffer, pcTaskGetName(xTask)); + if (taskInfo != NULL) { + unsigned long execTime, cpuUsage; + int parsedItems = sscanf(taskInfo, "%*s %*s %*s %*s %*s %*s %*s %lu %*s %lu", &execTime, &cpuUsage); + if (parsedItems == 2) { + ESP_LOGI(tag, "Task CPU Usage: %lu%%", cpuUsage); + } else { + ESP_LOGE(tag, "Failed to parse task statistics."); + } + } else { + ESP_LOGE(tag, "Task not found in statistics."); + } +} + +void addTaskHandleToList(SYS_TASK_HANDLES &list, TaskHandle_t handle){ + list.handle[list.count] = handle; + list.count++; +} + +void printTaskStackWatermark(TaskHandle_t xTask) { + UBaseType_t watermark = uxTaskGetStackHighWaterMark(xTask); + ESP_LOGI(tag, "Task Stack High Watermark: %u", watermark); +} + +// Stack, Heap and CPU Reporting +void report_system_stats() +{ + #if FREERTOS_DIAGNOSTICS == 1 + /* + // Get the number of tasks + UBaseType_t taskCount = uxTaskGetNumberOfTasks(); + UBaseType_t highWaterMark = uxTaskGetStackHighWaterMark(NULL); + // Allocate memory for task status array + TaskStatus_t *taskStatusArray = (TaskStatus_t*) malloc(sizeof(TaskStatus_t) * taskCount); + + // Get the system status and total CPU time + uint32_t totalCpuTime = 0; + uxTaskGetSystemState(taskStatusArray, taskCount, &totalCpuTime); + // Print task stats header + Serial.println("Task Name\tStack High Water Mark\tCPU Usage"); + + // Print task stats for each task + for (int i = 0; i < taskCount; i++) { + // Get the task status + TaskStatus_t taskStatus = taskStatusArray[i]; + + // Calculate task stack high water mark in bytes + uint32_t stackHighWaterMark = taskStatus.usStackHighWaterMark * sizeof(StackType_t); + + // Calculate task CPU usage as a percentage + uint32_t cpuUsage = (taskStatus.ulRunTimeCounter * 100) / totalCpuTime; + + // Print task stats + Serial.printf("%s\t\t%u bytes\t\t%u%%\n", taskStatus.pcTaskName, stackHighWaterMark, cpuUsage); + } + + // Free task status array memory + free(taskStatusArray); + */ + #endif +} + +String macToStr(uint8_t* mac){ + String macStr; + + for (int i = 0; i < 6; ++i) { + // Convert each byte to a hexadecimal string + // and append it to the String object + String section = String(mac[i], HEX); + if (section.length() == 1) { + section = "0" + section; + } + macStr += section; + + // Add colon between sections, but not after the last one + if (i < 5) { + macStr += ":"; + } + } + + // Convert the String to uppercase + macStr.toUpperCase(); + + return macStr; +} + +String getMacAddress(void) { + // Initialize an array to hold the MAC address + uint8_t macAddr[6]; + + // Get the MAC address for the Wi-Fi interface + WiFi.macAddress(macAddr); + + return macToStr(macAddr); +} + +bool updateJsonDocument(JsonDocument &doc, const char* filePath) { + // Check for available space + //if (LittleFS.totalBytes() - LittleFS.usedBytes() < doc.memoryUsage()) { + // ESP_LOGE(tag, "Not enough space to update file"); + // return false; + //} + + String tempFilePath = String(filePath) + ".tmp"; + String backupFilePath = String(filePath) + ".bak"; + + File tempFile = LittleFS.open(tempFilePath, "w"); + if (!tempFile) { + ESP_LOGE(tag, "Failed to create temporary file"); + return false; + } + + if (serializeJsonPretty(doc, tempFile) == 0) { + ESP_LOGE(tag, "Failed to write to temporary file"); + tempFile.close(); + LittleFS.remove(tempFilePath); + return false; + } + tempFile.close(); + + // Rename original file to backup (instead of deleting it) + if (LittleFS.exists(filePath)) { + if (!LittleFS.rename(filePath, backupFilePath)) { + ESP_LOGE(tag, "Failed to rename original file to backup"); + LittleFS.remove(tempFilePath); + return false; + } + } + + // Rename temporary file to original file name + if (!LittleFS.rename(tempFilePath, filePath)) { + ESP_LOGE(tag, "Failed to rename temporary file to original"); + LittleFS.rename(backupFilePath, filePath); // Attempt to restore from backup + return false; + } + + // Remove backup file + LittleFS.remove(backupFilePath); + + ESP_LOGD(tag, "File successfully updated"); + return true; +} + + +#include "soc/ledc_struct.h" +#define LEDC_CHANNEL_COUNT 8 // ESP32 supports up to 8 channels (0 to 7) +int findUnusedLedcChannel() { + // Iterate over all LEDC channels + for (int channel = 0; channel < LEDC_CHANNEL_COUNT; ++channel) { + // Check group 0 registers for channel usage + if (LEDC.channel_group[0].channel[channel].conf0.val == 0) { + return channel; // Found an unused channel + } + } + return -1; // Return -1 if no unused channel is found +} + + + + +#include "esp_timer.h" +#include "freertos/task.h" +void Log_CPU_Load(void) { + static const char* tag = "CPU_Load"; + + // Get run time stats + char *stats = (char*)malloc(2048); + if (!stats) { + ESP_LOGE(tag, "Failed to allocate memory for stats"); + return; + } + + vTaskGetRunTimeStats(stats); + + // Calculate percentage load for each core + uint32_t total_runtime = 0; + uint32_t idle_0 = 0; + uint32_t idle_1 = 0; + + TaskStatus_t *task_array; + UBaseType_t task_count; + + // Get task array + task_count = uxTaskGetNumberOfTasks(); + task_array = (TaskStatus_t*)malloc(sizeof(TaskStatus_t) * task_count); + + if (!task_array) { + free(stats); + ESP_LOGE(tag, "Failed to allocate memory for task array"); + return; + } + + // Get task stats + task_count = uxTaskGetSystemState(task_array, task_count, &total_runtime); + + // Find IDLE tasks + for (UBaseType_t i = 0; i < task_count; i++) { + if (strcmp(task_array[i].pcTaskName, "IDLE0") == 0) { + idle_0 = task_array[i].ulRunTimeCounter; + } + else if (strcmp(task_array[i].pcTaskName, "IDLE1") == 0) { + idle_1 = task_array[i].ulRunTimeCounter; + } + } + + // Calculate loads + float core0_load = 100.0f - ((float)idle_0 * 100.0f / (float)total_runtime); + float core1_load = 100.0f - ((float)idle_1 * 100.0f / (float)total_runtime); + + // Log results + ESP_LOGI(tag, "CPU Load - Core 0: %.1f%%, Core 1: %.1f%%", core0_load, core1_load); + + // Cleanup + free(stats); + free(task_array); +} + +/* +void print_task_watermarks(void) { + static const char* tag = "TaskWatermarks"; + UBaseType_t taskCount = uxTaskGetNumberOfTasks(); + TaskStatus_t* taskStatusArray = (TaskStatus_t*)pvPortMalloc(taskCount * sizeof(TaskStatus_t)); + + if (taskStatusArray != NULL) { + UBaseType_t actualTaskCount = uxTaskGetSystemState(taskStatusArray, taskCount, NULL); + + ESP_LOGI(tag, "Task Stack Watermarks:"); + ESP_LOGI(tag, "%-20s %-10s %-10s %-10s", "Task Name", "Priority", "Stack", "Watermark"); + + for (UBaseType_t i = 0; i < actualTaskCount; i++) { + UBaseType_t watermark = taskStatusArray[i].usStackHighWaterMark; + const char* warning = ""; + + // Warning levels + if (watermark < 50) { + warning = " !!! CRITICAL !!!"; + } else if (watermark < 100) { + warning = " ! WARNING !"; + } + + ESP_LOGI(tag, "%-20s %-10d %-10d %-10d%s", + taskStatusArray[i].pcTaskName, + (int)taskStatusArray[i].uxCurrentPriority, + (int)taskStatusArray[i].usStackHighWaterMark * sizeof(StackType_t), + (int)watermark, + warning); + } + + vPortFree(taskStatusArray); + } else { + ESP_LOGE(tag, "Failed to allocate memory for task status array"); + } +} +*/ + +/* +void print_task_watermarks(void) { + static const char* tag = "TaskWatermarks"; + char* buf = (char*)malloc(2048); + + if (buf) { + vTaskList(buf); + ESP_LOGI(tag, "\nTask Name\tStatus\tPrio\tHWM\tTask#\n%s", buf); + free(buf); + } else { + ESP_LOGE(tag, "Failed to allocate buffer for task list"); + } +} +*/ + +void print_task_watermarks(void) { + static const char* tag = "TaskWatermarks"; + + // Get current task handle + TaskHandle_t currentTask = xTaskGetCurrentTaskHandle(); + + if (currentTask != NULL) { + // Get task info + const char* name = pcTaskGetName(currentTask); + UBaseType_t priority = uxTaskPriorityGet(currentTask); + eTaskState state = eTaskGetState(currentTask); + UBaseType_t watermark = uxTaskGetStackHighWaterMark(currentTask); + + // Print header + ESP_LOGI(tag, "\nTask Statistics"); + ESP_LOGI(tag, "%-16s %-8s %-8s %-8s %s", "Name", "State", "Prio", "HWM", "Status"); + ESP_LOGI(tag, "---------------------------------------------------"); + + // Set warning level + const char* warning = ""; + if (watermark < 50) warning = " !CRITICAL!"; + else if (watermark < 100) warning = " !WARNING!"; + + // Print task info + ESP_LOGI(tag, "%-16s %-8d %-8d %-8d%s", + name, + state, + priority, + watermark, + warning); + } else { + ESP_LOGE(tag, "Failed to get current task handle"); + } +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2bdcad7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,595 @@ +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +// #include "freertos/FreeRTOSConfig.h" +#include "sdkconfig.h" +#include + +#include +#include +#include "soc/ledc_reg.h" +#include "soc/ledc_struct.h" +#include "esp_adc_cal.h" +#include "esp_log.h" + +#include +#include +#include +#include "system.h" +#include "JsonConstrain.h" +#include "common/fileSystem.h" +#include "my_board.h" +#include "global.h" +#include "my_wifi.h" +#include "my_buzzer.h" +#include "my_tsensor.h" +#include "my_buttons.h" +#include "PWM_Output.h" +#include "Ramp_Lights.h" +#include "BleServer.h" +#include "ATALights.h" +#include "OnEveryN.h" +#include "BLE_SP110E.h" + +#define FREERTOs_DIAGNOSTICS 0 +#define OLED_ENABLED 0 +#define WIFI_ENABLED 0 +#define STRIPS_ENABLED 1 +#define LUMASTIK_ENABLED 0 +#define PRINT_SYSTEM_STATUS 0 + +#if OLED_ENABLED +#include "my_oled.h" +#endif + +#if WIFI_ENABLED +#include "my_wifi.h" +#endif + +#if STRIPS_ENABLED +#include "led_strip.h" +#endif + +#if LUMASTIK_ENABLED +#include "luma_master.h" +#endif + +#if LUMASTIK_ENABLED +#define LumaCountReset (20000 / 25) +int LumaCountdown = LumaCountReset; +LUMA_PACKET lumaPacket; +#endif + +static const char *tag = "main"; +SYS_SETTINGS sys_settings; +PWM_Output *pwmOutputs[4]; +RAMP_LIGHT *rampLight1; +RAMP_LIGHT *rampLight2; + +void Init_ADC(void); +float readBoardInputVoltage(void); +void setupLogLevels(esp_log_level_t logLevel); +void Get_Board_and_Booth_File_Paths(const char *, String &, String &); +void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath); +void Init_PWM_Outputs(int8_t (&pin)[4], PWM_OUT_SETTINGS (&pwmSettings)[4]); +void Init_Ramp_Lights(RAMP_LIGHT_SETTINGS (&settings)[2], OneButton *(&btn)[3], PWM_Output *(&pwm)[4]); + +void checkLEDCChannels() +{ + for (int i = 0; i < 8; i++) + { + if (LEDC.channel_group[0].channel[i].conf0.val != 0 || LEDC.channel_group[1].channel[i].conf0.val != 0) + { + ESP_LOGD(tag, "Channel %d is configured\n", i); + } + } +} + +#define Button1Pin 8 +void setup() +{ + Serial.begin(115200); + while (!Serial) + ; + + // Initialize I2C Port for TSensor, ... + Wire.begin(I2C_SDA1_Pin, I2C_SCL1_Pin); + + sys_settings.ledStripSettings[0] = &ledSettings[0]; + sys_settings.ledStripSettings[1] = &ledSettings[1]; + + // Set Logging Levels + // esp_log_level_t logLevel = ESP_LOG_INFO; + // pinMode(Button1Pin, INPUT); // Button1 + // if(!digitalRead(Button1Pin)){ + // logLevel = ESP_LOG_VERBOSE; + //} + // setupLogLevels(logLevel); + setupLogLevels(ESP_LOG_INFO); + + // chip id, mac, + print_chip_info(); + + // Init LittleFS + Init_File_System(); + + String board_file_path, booth_file_path; + Get_Board_and_Booth_File_Paths("/system/system.json", board_file_path, booth_file_path); + + // Load Board Pins + Load_Board_Pins(sys_settings.boardPins, board_file_path); + + // Load Booth Settings + Load_Booth_Settings(sys_settings, booth_file_path); + + // Load Wifi Settings + Wifi_Load_Settings("/system/wifi.json"); + + // config stat, relays, buttons + Init_Board_Basic(sys_settings.boardPins); + + // Load tunes.json and initialize + Init_Buzzer(sys_settings.boardPins.buzzer, "/system/tunes.json"); + + // Initialize PWM Outputs + Init_PWM_Outputs(sys_settings.boardPins.relay, sys_settings.pwmOutSettings); + + // activate button clicks + Init_ButtonEvents(sys_settings.boardPins.btn); + + // Initialize Ramp Lights + Init_Ramp_Lights(sys_settings.rampLightSettings, boardButtons, pwmOutputs); + + // Initialize Temperature Sensor + Init_TSensor(72); + + Init_ADC(); + + float val = readBoardInputVoltage(); + ESP_LOGI(tag, "Input Volage = %f", val); + + // Initialize BLE + if (digitalRead(sys_settings.boardPins.btn[0]) == LOW) + { + ESP_LOGW(tag, "Enabling BLE and Update Service"); + Init_BleServer(true, true); + ESP_LOGW(tag, "Enabling Wifi AP and Client"); + Wifi_Init(); + } + else + { + Init_BleServer(true, false); // Dont start the Upgrade service + } + + // If lightstick mode is enabled, start ble lightstick client task + if (sys_settings.mode == BOOTH_MODE_STIK) + { + Init_BLE_LightStick_Client(); + } + +#if OLED_ENABLED + // Init OLED + Init_OLED(sys_settings.oledSettings.width, sys_settings.oledSettings.height, sys_settings.boardPins.oled_mosi, sys_settings.boardPins.oled_sck, sys_settings.boardPins.oled_dc, sys_settings.boardPins.oled_rst, sys_settings.boardPins.oled_cs); +#endif + +#if STRIPS_ENABLED + Init_Lights_Task(); +#endif + + Buzzer_Play_Tune(TUNE_BOOT, true, true); + + // TODO... Test if this is still necessary need to configure pin 0 for some reason + // pinMode(0, INPUT); // button0/boot pin + +#if LUMASTIK_ENABLED + Init_Luma_Master(); +#endif + + vTaskDelay(100); + Lights_Control_Task_Resume(); +} + +void loop() +{ + + // Button Scanning + ON_EVERY_N_MILLISECONDS(10) + { + if (boardButtons[0] != NULL) + { + boardButtons[0]->tick(); + } + if (boardButtons[1] != NULL) + { + boardButtons[1]->tick(); + } + if (boardButtons[2] != NULL) + { + boardButtons[2]->tick(); + } + } + + // Temperature Monitor + ON_EVERY_N_MILLISECONDS(5000) + { + static float boardTemperature; + + // Read temperature if the sensor is enabled + if (sys_settings.tSensorSettings.enabled) + { + boardTemperature = tSensor->readTemperatureF(); + // ESP_LOGD(tag, "Board T: %F", boardTemperature); + } + + // Fan Control + if (sys_settings.tSensorSettings.enabled) + { + UpdateFanControl(boardTemperature, pwmOutputs[sys_settings.tSensorSettings.pwmIndex]); + } + } + + // Update Tune Playing + if (anyrtttl::nonblocking::isPlaying()) + { + anyrtttl::nonblocking::play(); + } + +// Animation TestMode Timeout +#if LEDS_ENABLED + if (animStatus.EventTestCountdown) + { + if (--animStatus.EventTestCountdown == 0) + { + ESP_LOGD(tag, "Test Timeout trigger"); + PostLastNormalEvent(); + if (Strip1_Task_Handle) + { + xTaskNotifyGive(Strip1_Task_Handle); + } // trigger exit of animation loop + } + }; +#endif + +// Reboot requested +#if WIFI_ENABLED + if (RebootSystem) + { + if (--RebootSystem == 0) + { + for (int i = 0; i < 3; i++) + { +#if BUZZER_ENABLED + Buzzer_Play_Tune(TUNE_BEEP, false); // blocking +#endif + vTaskDelay(200); + } + ESP_LOGW(tag, "Restarting..."); + vTaskDelay(200); + ESP.restart(); + } + } +#endif + + // Toggle Status LED L2 + ON_EVERY_N_MILLISECONDS(500) + { + if (sys_settings.boardPins.stat[1] >= 0) + { + static bool ledState = false; + // digitalWrite(sys_settings.boardPins.stat[1], ledState = !ledState); + setStatusPin2(ledState = !ledState) + } + } + +#if FREERTOs_DIAGNOSTICS + ON_EVERY_N_MILLISECONDS(60000) + { + print_task_watermarks(); + } +#endif +} + +void setupLogLevels(esp_log_level_t logLevel) +{ + // Options: ESP_LOG_ERROR,ESP_LOG_WARN,ESP_LOG_INFO,ESP_LOG_DEBUG,ESP_LOG_VERBOSE + esp_log_level_set("*", logLevel); + esp_log_level_set("fs", logLevel); + esp_log_level_set("main", logLevel); + esp_log_level_set("board", logLevel); + esp_log_level_set("ramp", logLevel); + esp_log_level_set("button", logLevel); + esp_log_level_set("buzzer", logLevel); + esp_log_level_set("global", logLevel); + esp_log_level_set("jsCon", logLevel); + esp_log_level_set("ble", logLevel); + esp_log_level_set("pwmout", logLevel); + esp_log_level_set("wifi", logLevel); + esp_log_level_set("stiks", logLevel); + esp_log_level_set("oled", logLevel); + esp_log_level_set("trx433", logLevel); + esp_log_level_set("tsensor", logLevel); +} + +// TODO Restore original setOutput code.. +#define RELAY_RES 10 +void Init_PWM_Outputs(int8_t (&pin)[4], PWM_OUT_SETTINGS (&pwmSettings)[4]) +{ + for (int i = 0; i < 4; i++) + { + int chIndex = findUnusedLedcChannel(); + if (chIndex < 0) + { + ESP_LOGE(tag, "No available LEDC channel for PWM Output%d", i); + continue; + } + pwmOutputs[i] = new PWM_Output(pin[i], chIndex, RELAY_RES, pwmSettings[i].freq, pwmSettings[i].max, false); + pwmOutputs[i]->setOutput(pwmSettings[i].def); + // pwmOutputs[i]->setOutput(5.0); + ESP_LOGD(tag, "PWM Output%d: Pin=%d, Freq=%d, ch=%d", i, pin[i], pwmSettings[i].freq, chIndex); + } +} + +void Init_Ramp_Lights(RAMP_LIGHT_SETTINGS (&settings)[2], OneButton *(&btn)[3], PWM_Output *(&pwm)[4]) +{ + if (settings[0].enabled) + { + rampLight1 = new RAMP_LIGHT(btn[settings[0].btnIndex], pwm[settings[0].pwmOutIndex], 5.0, 100.0, 1.5); + ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d", 1, settings[0].btnIndex, settings[0].pwmOutIndex); + } + + if (settings[1].enabled) + { + rampLight2 = new RAMP_LIGHT(btn[settings[1].btnIndex], pwm[settings[1].pwmOutIndex], 5.0, 100.0, 1.5); + ESP_LOGD(tag, "RampLight%d: btn=%d, pwmIndex=%d", 2, settings[1].btnIndex, settings[1].pwmOutIndex); + } +} + +// Get the files that should be used to setup the system +void Get_Board_and_Booth_File_Paths(const char *sysPath, String &boardPath, String &boothPath) +{ + File file = LittleFS.open(sysPath); + + if (!file) + { + ESP_LOGE(tag, "Error opening %s...", sysPath); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) + { + ESP_LOGE(tag, "%s deserialize error!..", sysPath); + return; + } + + // get hardware version string + boardPath = jsonConstrainString(tag, doc.as(), "boardfile", "/cfg/boards/board15.json"); + boothPath = jsonConstrainString(tag, doc.as(), "configfile", "/cfg/booths/custom.json"); +} + +void Load_Booth_Settings(SYS_SETTINGS &sys, const String &boothPath) +{ + File file = LittleFS.open(boothPath); + if (!file) + { + ESP_LOGE(tag, "Error opening %s...", boothPath.c_str()); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) + { + ESP_LOGE(tag, "%s deserialize error!..", boothPath.c_str()); + return; + } + + // ********** Mode *********** + String modeStr = jsonConstrainString(tag, doc.as(), "mode", "booth"); + if (modeStr == "roamer") + { + sys_settings.mode = BOOTH_MODE_ROAMER; + } + else if (modeStr == "stik") + { + sys_settings.mode = BOOTH_MODE_STIK; + } + else + { + sys_settings.mode = BOOTH_MODE_NONE; + } + + // ********** PWM Out *********** + JsonArray pwmJsonArray = doc["pwmout"]; + if (!pwmJsonArray.isNull()) + { + int pwmIndex = 0; + for (JsonObject obj : pwmJsonArray) + { + if (pwmIndex >= 4) + break; + sys_settings.pwmOutSettings[pwmIndex].enabled = jsonConstrainBool(tag, obj, "en", true); + sys_settings.pwmOutSettings[pwmIndex].freq = jsonConstrain(tag, obj, "freq", 100, 5000, 250); + sys_settings.pwmOutSettings[pwmIndex].min = jsonConstrain(tag, obj, "min", 0.0, 95.0, 0.0); + sys_settings.pwmOutSettings[pwmIndex].max = jsonConstrain(tag, obj, "max", 5.0, 100.0, 100.0); + sys_settings.pwmOutSettings[pwmIndex].def = jsonConstrain(tag, obj, "default", 0.0, 100.0, 0.0); + sys_settings.pwmOutSettings[pwmIndex].deltaRate = 1.0; + // sys_settings.pwmOutSettings[pwmIndex]. = jsonConstrainBool(tag, obj, "vision", true); + pwmIndex++; + } + ESP_LOGI(tag, "Loaded PWmOutput settings..."); + } + else + { + ESP_LOGE(tag, "Error!, %s key: pwmout not found..", boothPath); + } + + // ********** Ramp Lights *********** + JsonArray rampJsonArray = doc["ramp-lights"]; + if (!rampJsonArray.isNull()) + { + int rampIndex = 0; + for (JsonObject obj : rampJsonArray) + { + if (rampIndex >= 2) + break; + sys_settings.rampLightSettings[rampIndex].enabled = jsonConstrainBool(tag, obj, "en", true); + sys_settings.rampLightSettings[rampIndex].vision = jsonConstrainBool(tag, obj, "vision", true); + sys_settings.rampLightSettings[rampIndex].pwmOutIndex = jsonConstrain(tag, obj, "relay-index", 0, 1, 0); + sys_settings.rampLightSettings[rampIndex].btnIndex = jsonConstrain(tag, obj, "button-index", 0, 1, 0); + sys_settings.rampLightSettings[rampIndex].min = jsonConstrain(tag, obj, "min", 0.0, 100.0, 0.0); + sys_settings.rampLightSettings[rampIndex].max = jsonConstrain(tag, obj, "max", 5.0, 100.0, 100.0); + sys_settings.rampLightSettings[rampIndex].step = jsonConstrain(tag, obj, "step", 0.01, 100.0, 1.5); + rampIndex++; + } + ESP_LOGI(tag, "Loaded Ramp Lights settings..."); + } + else + { + ESP_LOGE(tag, "Error!, %s key: ramp-lights not found..", boothPath); + } + + // ********** Fan *********** + JsonObject sensorJson = doc["t-sensor"]; + if (!sensorJson.isNull()) + { + sys_settings.tSensorSettings.enabled = jsonConstrainBool(tag, sensorJson, "en", true); + sys_settings.tSensorSettings.pwmIndex = jsonConstrain(tag, sensorJson, "relay", 0, 3, 3); + sys_settings.tSensorSettings.setpoint1 = jsonConstrain(tag, sensorJson, "sp1", 0.0, 100.0, 80.0); + sys_settings.tSensorSettings.setpoint2 = jsonConstrain(tag, sensorJson, "sp2", 0.0, 100.0, 85.0); + sys_settings.tSensorSettings.fanPower1 = jsonConstrain(tag, sensorJson, "fan-pwr1", 0.0, 100.0, 50.0); + sys_settings.tSensorSettings.fanPower2 = jsonConstrain(tag, sensorJson, "fan-pwr2", 0.0, 100.0, 50.0); + sys_settings.tSensorSettings.hyst = jsonConstrain(tag, sensorJson, "hyst", 1.0, 10.0, 1.0); + sys_settings.tSensorSettings.intervalMs = jsonConstrain(tag, sensorJson, "interval", 1000, 30000, 5000); + ESP_LOGI(tag, "Loaded TSensor settings..."); + ESP_LOGD(tag, " SP1: %F, SP2 %F, Hyst: %F", sys_settings.tSensorSettings.setpoint1, sys_settings.tSensorSettings.setpoint2, sys_settings.tSensorSettings.hyst); + } + else + { + ESP_LOGE(tag, "Error!, %s key: t-sensor not found..", boothPath); + } + + // ********** RGB Strips *********** + JsonArray stripsJsonArray = doc["strips"]; + if (!stripsJsonArray.isNull()) + { + int stripIndex = 0; + for (JsonObject obj : stripsJsonArray) + { + if (stripIndex >= 2) + break; + sys_settings.ledStripSettings[stripIndex]->enabled = jsonConstrainBool(tag, obj, "en", true); + sys_settings.ledStripSettings[stripIndex]->size = jsonConstrain(tag, obj, "size", 1, 250, 25); + sys_settings.ledStripSettings[stripIndex]->chip = jsonConstrainString(tag, obj, "chip", "WS2812B"); + sys_settings.ledStripSettings[stripIndex]->rgbOrder = jsonConstrainString(tag, obj, "rgb-order", "WS2812B"); + sys_settings.ledStripSettings[stripIndex]->shift = jsonConstrain(tag, obj, "shift", -250, 250, 0); + sys_settings.ledStripSettings[stripIndex]->offset = jsonConstrain(tag, obj, "offset", -250, 250, 0); + sys_settings.ledStripSettings[stripIndex]->bright = jsonConstrain(tag, obj, "bright", 5, 255, 200); + sys_settings.ledStripSettings[stripIndex]->powerDiv = 0; + sys_settings.ledStripSettings[stripIndex]->i2sCh = 0; + sys_settings.ledStripSettings[stripIndex]->core = jsonConstrain(tag, obj, "core", 0, 1, 0); + stripIndex++; + } + + sys_settings.ledStripSettings[0]->pin = sys_settings.boardPins.rgb1; + sys_settings.ledStripSettings[1]->pin = sys_settings.boardPins.rgb2; + ESP_LOGI(tag, "Loaded LED Strip settings..."); + } + else + { + ESP_LOGE(tag, "Error!, %s key: strips not found.."); + } + + // ********** BLE *********** + JsonObject bleJson = doc["ble"]; + if (!bleJson.isNull()) + { + sys_settings.bleSettings.enabled = jsonConstrainBool(tag, bleJson, "en", true); + sys_settings.bleSettings.name = jsonConstrainString(tag, bleJson, "name", "ATA_LIGHTS"); + + ESP_LOGI(tag, "Loaded BLE settings..."); + } + else + { + ESP_LOGE(tag, "Error!, %s key: ble not found..", boothPath); + } + + // ********** RF Remote*********** + /* + JsonObject rfJson = doc["wifi-ap"]; + if(!rfJson.isNull()){ + //sys_settings.rampLights[0].enabled = jsonConstrainBool(tag, wifiApJson, "en", true); + ESP_LOGI(tag, "Loaded RF Remote settings..."); + }else{ + ESP_LOGE(tag, "Error!, %s key: wifi-ap not found..", boothPath); + } + */ +} + +void Init_ADC(void) +{ + // Configure ADC + analogReadResolution(12); // 12-bit ADC + analogSetAttenuation(ADC_11db); + if (sys_settings.boardPins.adc1 >= 0) + { + analogSetPinAttenuation(sys_settings.boardPins.adc1, ADC_11db); + } + + // Enable ADC calibration + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 0, &adc_chars); + + // Check calibration success + if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_TP) == ESP_OK) + { + ESP_LOGI(tag, "ADC calibration: Using Two Point values from eFuse"); + } + else if (esp_adc_cal_check_efuse(ESP_ADC_CAL_VAL_EFUSE_VREF) == ESP_OK) + { + ESP_LOGI(tag, "ADC calibration: Using reference voltage from eFuse"); + } + else + { + ESP_LOGW(tag, "ADC calibration: Using default reference voltage"); + } +} + +float readBoardInputVoltage(void) +{ + const int SAMPLES = 64; + uint32_t reading = 0; + + if (sys_settings.boardPins.adc1 < 0) + { + ESP_LOGE(tag, "ADC Pin not valid"); + return 0.0; + } + + // Multiple readings for averaging + for (int i = 0; i < SAMPLES; i++) + { + reading += analogRead(sys_settings.boardPins.adc1); + delayMicroseconds(50); // Small delay between samples + } + reading /= SAMPLES; + + // Convert raw ADC to voltage using calibration + uint32_t voltage_mv; + esp_adc_cal_characteristics_t adc_chars; + esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars); + voltage_mv = esp_adc_cal_raw_to_voltage(reading, &adc_chars); + + // Scale to 12V range + float voltage = (voltage_mv / 1000.0f) * (10470.0f / 470.0f); + + return voltage; +} \ No newline at end of file diff --git a/src/my_board.cpp b/src/my_board.cpp new file mode 100644 index 0000000..f0bbcdb --- /dev/null +++ b/src/my_board.cpp @@ -0,0 +1,92 @@ +#include "my_board.h" + +#include +#include +#include + +#include "global.h" +#include "JsonConstrain.h" +#include "system.h" + +static const char* tag = "board"; + +BOARD_PINS* thisBoardPins; + +void Load_Board_Pins(BOARD_PINS& boardPins, String& path){ + thisBoardPins = &boardPins; + File file = LittleFS.open(path); + + if (!file) { + ESP_LOGE(tag, "Error opening %s...", path.c_str()); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + ESP_LOGE(tag, "%s deserialize error!..", path.c_str()); + return; + } + + JsonObject boardJson = doc.as(); + boardPins.rgb1 = jsonConstrain(tag, boardJson, "rgb1", -1, 48, -1); + boardPins.rgb2 = jsonConstrain(tag, boardJson, "rgb2", -1, 48, -1); + boardPins.btn[0] = jsonConstrain(tag, boardJson, "btn1", -1, 48, -1); + boardPins.btn[1] = jsonConstrain(tag, boardJson, "btn2", -1, 48, -1); + boardPins.btn[2] = jsonConstrain(tag, boardJson, "btn3", -1, 48, -1); + boardPins.buzzer = jsonConstrain(tag, boardJson, "buzzer", -1, 48, -1); + boardPins.touch[0] = jsonConstrain(tag, boardJson, "touch1", -1, 48, -1); + boardPins.touch[1] = jsonConstrain(tag, boardJson, "touch2", -1, 48, -1); + boardPins.touch[2] = jsonConstrain(tag, boardJson, "touch3", -1, 48, -1); + boardPins.touch[3] = jsonConstrain(tag, boardJson, "touch4", -1, 48, -1); + boardPins.touch[4] = jsonConstrain(tag, boardJson, "touch5", -1, 48, -1); + boardPins.shield = jsonConstrain(tag, boardJson, "shield", -1, 48, -1); + boardPins.relay[0] = jsonConstrain(tag, boardJson, "relay1", -1, 48, -1); + boardPins.relay[1] = jsonConstrain(tag, boardJson, "relay2", -1, 48, -1); + boardPins.relay[2] = jsonConstrain(tag, boardJson, "relay3", -1, 48, -1); + boardPins.relay[3] = jsonConstrain(tag, boardJson, "relay4", -1, 48, -1); + boardPins.stat[0] = jsonConstrain(tag, boardJson, "stat1", -1, 48, -1); + boardPins.stat[1] = jsonConstrain(tag, boardJson, "stat2", -1, 48, -1); + boardPins.adc1 = jsonConstrain(tag, boardJson, "adc1", -1, 48, -1); + boardPins.oled_dc = jsonConstrain(tag, boardJson, "oled_dc", -1, 48, -1); + boardPins.oled_rst = jsonConstrain(tag, boardJson, "oled_rst", -1, 48, -1); + boardPins.oled_mosi = jsonConstrain(tag, boardJson, "oled_mosi", -1, 48, -1); + boardPins.oled_sck = jsonConstrain(tag, boardJson, "oled_sck", -1, 48, -1); + boardPins.oled_cs = jsonConstrain(tag, boardJson, "oled_cs", -1, 48, -1); + boardPins.ext[0] = jsonConstrain(tag, boardJson, "ext1", -1, 48, -1); + boardPins.ext[1] = jsonConstrain(tag, boardJson, "ext2", -1, 48, -1); + boardPins.rf433tx = jsonConstrain(tag, boardJson, "rf433tx", -1, 48, -1); + boardPins.rf433rx = jsonConstrain(tag, boardJson, "rf433rx", -1, 48, -1); + + // TODO Add hardware version to log + ESP_LOGI(tag, "loaded Pins from %s", path.c_str()); + +} + + +void Init_Board_Basic(BOARD_PINS& boardPins) +{ + + if(boardPins.stat[0] >= 0){ pinMode(boardPins.stat[0], OUTPUT); } + if(boardPins.stat[1] >= 0){ pinMode(boardPins.stat[1], OUTPUT); } + + if(boardPins.btn[0] >= 0){ pinMode(boardPins.btn[0], INPUT_PULLUP); } + if(boardPins.btn[1] >= 0){ pinMode(boardPins.btn[1], INPUT_PULLUP); } + if(boardPins.btn[2] >= 0){ pinMode(boardPins.btn[2], INPUT); } + + if(boardPins.rgb1 >= 0){ pinMode(boardPins.rgb1, OUTPUT); } + if(boardPins.rgb2 >= 0){ pinMode(boardPins.rgb2, OUTPUT); } + + if(boardPins.relay[0] >= 0){ pinMode(boardPins.relay[0], OUTPUT); } + if(boardPins.relay[1] >= 0){ pinMode(boardPins.relay[1], OUTPUT); } + if(boardPins.relay[2] >= 0){ pinMode(boardPins.relay[2], OUTPUT); } + if(boardPins.relay[3] >= 0){ pinMode(boardPins.relay[3], OUTPUT); } + + ESP_LOGI(tag, "Board pins initialized..."); +} + + + + diff --git a/src/my_buttons.cpp b/src/my_buttons.cpp new file mode 100644 index 0000000..5de9ae9 --- /dev/null +++ b/src/my_buttons.cpp @@ -0,0 +1,137 @@ +#include "my_buttons.h" +#include "global.h" +#include "BLE_UpdateService.h" + +static const char* tag = "button"; +OneButton *boardButtons[3]; + +void Init_ButtonEvents(int8_t (&pin)[3]){ + + + if(pin[0] >= 0) { + boardButtons[0] = new OneButton(pin[0], true, true); + ESP_LOGD(tag, "Button1 Events, pin=%d", pin[0]); + } + if(pin[1] >= 0) { + boardButtons[1] = new OneButton(pin[1], true, true); + ESP_LOGD(tag, "Button2 Events, pin=%d", pin[1]); + } + if(pin[2] >= 0) { + boardButtons[2] = new OneButton(pin[2], true, false); + ESP_LOGD(tag, "Button3 Events, pin=%d", pin[2]); + } + + /* + if(boardButtons[0] != NULL){ + //boardButtons[0]->setDebounceTicks(DEBOUCE_TIME); // just below the update period to guarantee 1 sample delay + boardButtons[0]->attachClick(btn1_click); + boardButtons[0]->attachDoubleClick(btn1_doubleClick); + boardButtons[0]->attachLongPressStart(btn1_LongPressStart); + boardButtons[0]->attachLongPressStop(btn1_LongPressStop); + boardButtons[0]->attachDuringLongPress(btn1_DuringLongPress); + } + else { + ESP_LOGD(tag, "Button1 Not Initialized"); + } + + if(boardButtons[1] != NULL){ + //boardButtons[1]->setDebounceTicks(DEBOUCE_TIME); + boardButtons[1]->attachClick(btn2_click); + boardButtons[1]->attachDoubleClick(btn2_doubleClick); + boardButtons[1]->attachLongPressStart(btn2_LongPressStart); + boardButtons[1]->attachLongPressStop(btn2_LongPressStop); + boardButtons[1]->attachDuringLongPress(btn2_DuringLongPress); + } + else { + ESP_LOGD(tag, "Button2 Not Initialized"); + } +*/ + + if(boardButtons[2] != NULL){ + //boardButtons[2]->setDebounceTicks(DEBOUCE_TIME); + boardButtons[2]->attachClick(btn3_click); + boardButtons[2]->attachDoubleClick(btn3_doubleClick); + boardButtons[2]->attachLongPressStart(btn3_LongPressStart); + boardButtons[2]->attachLongPressStop(btn3_LongPressStop); + boardButtons[2]->attachDuringLongPress(btn3_DuringLongPress); + ESP_LOGD(tag, "Button3 Events Initialized"); + } + else { + ESP_LOGD(tag, "Button3 Not Initialized"); + } + +} + +void btn1_click() { + //IncrementEventIndex(); + //Pulse_LED_Status(150); + ESP_LOGD(tag, "btn1 1x"); +} + +void btn1_doubleClick() { + ESP_LOGD(tag, "btn1 2x"); +} + +void btn1_LongPressStart(){ + ESP_LOGD(tag, "btn1 long press start"); +} + +void btn1_LongPressStop(){ + ESP_LOGD(tag, "btn1 long press stop"); +} + +void btn1_DuringLongPress(){ + ESP_LOGD(tag, "btn1 long press"); +} + + +/**************************/ + +void btn2_click() { + //Pulse_LED_Status(150); + //Buzzer_Beep(150); + // send packet + ESP_LOGD(tag, "btn2 1x"); +} + +void btn2_doubleClick() { + ESP_LOGD(tag, "btn2 2x"); +} + +void btn2_LongPressStart(){ + ESP_LOGD(tag, "btn2 long press start"); +} + +void btn2_LongPressStop(){ + ESP_LOGD(tag, "btn2 long press stop"); +} + +void btn2_DuringLongPress(){ + ESP_LOGD(tag, "btn2 long press"); +} + + +/**************************/ + +void btn3_click() { + //Pulse_LED_Status(150); + ESP_LOGD(tag, "btn3 1x"); +} + +void btn3_doubleClick() { + bleUpgrade_send_message("Hello....3"); + ESP_LOGD(tag, "btn3 2x"); +} + +void btn3_LongPressStart(){ + ESP_LOGD(tag, "btn3 long press start"); +} + +void btn3_LongPressStop(){ + ESP_LOGD(tag, "btn3 long press stop"); +} + +void btn3_DuringLongPress(){ + ESP_LOGD(tag, "btn3 long press"); +} + diff --git a/src/my_buzzer.cpp b/src/my_buzzer.cpp new file mode 100644 index 0000000..5452cbc --- /dev/null +++ b/src/my_buzzer.cpp @@ -0,0 +1,109 @@ +#include "my_buzzer.h" +#include +#include + +#include +#include +#include + +#include "JsonConstrain.h" + +const char* DEFAULT_MELODY = "Ack:d=16,o=5,b=200:c,e,g"; + +// serial debugging enabled +//#define ANY_RTTTL_INFO + +static const char* tag = "buzzer"; + +BUZZ_TUNE buzzTune[TUNE_MAX_COUNT]; +int8_t buzzPin; + + +void Init_Buzzer(int8_t pin, const char* configFile) +{ + buzzPin = pin; + if(buzzPin >= 0){ + pinMode(buzzPin, OUTPUT); + } + + Buzzer_Load_Tunes(configFile); // Load Tunes + ESP_LOGD(tag, "Buzzer initialized.."); +} + +void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority) +{ + static int prev_tune = -1; + if(buzzPin < 0) return; + + if (hasPriority || !anyrtttl::nonblocking::isPlaying()) { + if (anyrtttl::nonblocking::isPlaying()) { + anyrtttl::nonblocking::stop(); + } + + // Load nonblocking if different from previous + if (prev_tune != tune && async) { + anyrtttl::nonblocking::begin(buzzPin, buzzTune[tune].melody.c_str()); + } + + ESP_LOGD(tag, "Playing tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str()); + + for (int c = 0; c < buzzTune[tune].cycles; c++) { + if (async) { + anyrtttl::nonblocking::play(); + } else { + anyrtttl::blocking::play(buzzPin, buzzTune[tune].melody.c_str()); + } + } + + prev_tune = tune; + } else { + ESP_LOGD(tag, "buzzer busy"); + } +} + +// TODO Buzzer Beep finish +void Buzzer_Beep(int mSecs, int freq) +{ + /* + ledcAttachPin(buzzPin, buzzerCh); + ledcSetup(buzzerCh, 2000, 8); + ledcWrite(buzzerCh, 125); + vTaskDelay(mSecs); + ledcWrite(buzzerCh, 0); + */ +} + +// TODO Reduce tunes to load () +void Buzzer_Load_Tunes(const char* tunesPath){ + File file = LittleFS.open(tunesPath); + + if (!file) { + ESP_LOGE(tag, "Error opening %s...", tunesPath); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if(error){ + ESP_LOGE(tag, "%s deserialize error!..", tunesPath); + return; + } + + JsonArray tuneJsonArray = doc["tunes"]; + if(!tuneJsonArray.isNull()){ + int tuneIndex = 0; + for(JsonObject obj : tuneJsonArray){ + if(tuneIndex >= TUNE_MAX_COUNT) break; + buzzTune[tuneIndex].cycles = jsonConstrain(tag, obj, "cycles", 1, 100, 1); + buzzTune[tuneIndex].pause = jsonConstrain(tag, obj, "pause", 0, 100, 0); + buzzTune[tuneIndex].melody = jsonConstrainString(tag, obj, "tune", DEFAULT_MELODY); + ESP_LOGD(tag, "tune %d : %s", tuneIndex, buzzTune[tuneIndex].melody.c_str()); + tuneIndex++; + } + ESP_LOGI(tag, "Loaded tunes..."); + }else{ + ESP_LOGE(tag, "Error!, %s key: tunes not found..", tunesPath); + } +} \ No newline at end of file diff --git a/src/my_oled.cpp b/src/my_oled.cpp new file mode 100644 index 0000000..1b8de4d --- /dev/null +++ b/src/my_oled.cpp @@ -0,0 +1,125 @@ +#include "my_oled.h" +#include +#include + +static const char* tag = "oled"; +Adafruit_SSD1306 *oled; + +void Init_OLED(uint8_t width, uint8_t height, uint8_t mosiPin, uint8_t sckPin, uint8_t dcPin, uint8_t rstPin, uint8_t csPin) +{ + oled = new Adafruit_SSD1306(width, height, mosiPin, sckPin, dcPin, rstPin, csPin); + + if(!oled->begin(SSD1306_SWITCHCAPVCC)) { + ESP_LOGE(tag, "SSD1306 allocation failed"); + for(;;); // Don't proceed, loop forever + } + + oled_ShowInfo(); +} + +void oled_ShowInfo(void){ + //if(sysProps.oledEnabled) { + oled->display(); + vTaskDelay(2000); // Pause for 2 seconds + + // Clear the buffer + oled->clearDisplay(); + + // Draw a single pixel in white + oled->drawPixel(10, 10, SSD1306_WHITE); + + // Show the display buffer on the screen. You MUST call display() after + // drawing commands to make them visible on screen! + oled->display(); + vTaskDelay(2000); + // display.display() is NOT necessary after every single drawing command, + // unless that's what you want...rather, you can batch up a bunch of + // drawing operations and then update the screen all at once by calling + // display.display(). These examples demonstrate both approaches... + + //testdrawline(*oled); // Draw many lines + + //testdrawrect(*oled); // Draw rectangles (outlines) + + testdrawline(oled); // Draw many lines + + testdrawrect(oled); // Draw rectangles (outlines) + //} +} + +void testdrawline(Adafruit_SSD1306* display) { + //if(sysProps.oledEnabled) { + int16_t i; + + display->clearDisplay(); // Clear display buffer + + for(i=0; iwidth(); i+=4) { + display->drawLine(0, 0, i, display->height()-1, SSD1306_WHITE); + display->display(); // Update screen with each newly-drawn line + vTaskDelay(1); + } + for(i=0; iheight(); i+=4) { + display->drawLine(0, 0, display->width()-1, i, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + vTaskDelay(250); + + display->clearDisplay(); + + for(i=0; iwidth(); i+=4) { + display->drawLine(0, display->height()-1, i, 0, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + for(i=display->height()-1; i>=0; i-=4) { + display->drawLine(0, display->height()-1, display->width()-1, i, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + vTaskDelay(250); + + display->clearDisplay(); + + for(i=display->width()-1; i>=0; i-=4) { + display->drawLine(display->width()-1, display->height()-1, i, 0, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + for(i=display->height()-1; i>=0; i-=4) { + display->drawLine(display->width()-1, display->height()-1, 0, i, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + vTaskDelay(250); + + display->clearDisplay(); + + for(i=0; iheight(); i+=4) { + display->drawLine(display->width()-1, 0, 0, i, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + for(i=0; iwidth(); i+=4) { + display->drawLine(display->width()-1, 0, i, display->height()-1, SSD1306_WHITE); + display->display(); + vTaskDelay(1); + } + + vTaskDelay(2000); // Pause for 2 seconds + //} +} + +void testdrawrect(Adafruit_SSD1306* display) { + //if(sysProps.oledEnabled) { + display->clearDisplay(); + + for(int16_t i=0; iheight()/2; i+=2) { + display->drawRect(i, i, display->width()-2*i, display->height()-2*i, SSD1306_WHITE); + display->display(); // Update screen with each newly-drawn rectangle + vTaskDelay(1); + } + + vTaskDelay(2000); + //} +} \ No newline at end of file diff --git a/src/my_tsensor.cpp b/src/my_tsensor.cpp new file mode 100644 index 0000000..bc2b4fa --- /dev/null +++ b/src/my_tsensor.cpp @@ -0,0 +1,48 @@ +#include "my_tsensor.h" +#include + +static const char* tag = "tsensor"; + +T_SENSOR tSensorSettings; +TI_TMP102_Compatible *tSensor; + +/******************* Temperature Control ********************/ + void Init_TSensor(uint8_t addr) { + //tSensor = new TI_TMP102_Compatible(72); + tSensor = new TI_TMP102_Compatible(addr); + } + + + void UpdateFanControl(float temperature, PWM_Output* pwmOut) { + static uint8_t FanState = 0; + float currentDuty = pwmOut->currDuty; + float newDuty = currentDuty; + + // Fan State Logic + if ((FanState == 2) && (temperature < (tSensorSettings.Setpoint2 - tSensorSettings.hyst))) { + newDuty = tSensorSettings.fanPower1; + FanState = 1; + //ESP_LOGD(tag, "Dropping down to FanPower1"); + } + else if ((FanState == 1) && (temperature < (tSensorSettings.Setpoint1 - tSensorSettings.hyst))) { + newDuty = 0; + FanState = 0; + //ESP_LOGD(tag, "Dropping down to FanPower0"); + } + else if ((FanState <= 1) && (temperature > tSensorSettings.Setpoint1)) { + newDuty = tSensorSettings.fanPower1; + if (temperature > tSensorSettings.Setpoint2) { + newDuty = tSensorSettings.fanPower2; + FanState = 2; + //ESP_LOGD(tag, "Raising up to FanPower2"); + } //else { + //ESP_LOGD(tag, "Raising up to FanPower1"); + //} + } + + // Apply new duty cycle if changed + if (currentDuty != newDuty) { + pwmOut->setOutput(newDuty); + ESP_LOGD(tag, "Board T: %.2f, Fan -> %.2f", temperature, newDuty); + } +} \ No newline at end of file diff --git a/src/my_wifi.cpp b/src/my_wifi.cpp new file mode 100644 index 0000000..f3cde55 --- /dev/null +++ b/src/my_wifi.cpp @@ -0,0 +1,1396 @@ +#include "my_wifi.h" +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include +#include +#include +#include "common/fileSystem.h" +#include "global.h" +#include "my_buzzer.h" +#include +#include +#include "jsonconstrain.h" +#include "AppUpgrade.h" +#include "my_board.h" + +static const char *tag = "WIFI"; + +volatile bool WifiClientConnected = false; +volatile bool InternetAvailable; +AsyncWebServer webServer(80); +AsyncEventSource eventUpgradeProgress("/upgrade-progress"); +// DNSServer *dnsServer; +// #define DNS_PORT 53 + +String client_ssid; +String client_pass; +String ap_ssid; +String ap_pass; + +String mDnsName; +String HostName; + +IPAddress local_IP(192, 168, 10, 1); +IPAddress gateway(192, 168, 10, 1); +IPAddress subnet(255, 255, 255, 0); + +// for file manager page +String filesDropdownOptions((char *)0); +String dirDropdownOptions((char *)0); +String savePath((char *)0); // needed for storing file when editing a file +String savePathInput((char *)0); +const char *http_username = "admin"; +const char *http_password = "admin"; +const char *param_delete_path = "delete-path"; +const char *param_edit_path = "edit-path"; +const char *param_dir_pad = "dir-path"; +const char *param_edit_textarea = "edit-textarea"; +const char *param_save_path = "save-path"; +String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg"; + +volatile bool scanInProgress = false; + +static String networkList = ""; +int networkCount = 0; + +volatile int scanStatus = 0; // 0=none, 1=scanning, 2=complete, -1=error +String scanResults = ""; // Store scan results globally + +TaskHandle_t Wifi_Task_Handle; +static const uint32_t CONNECT_TIMEOUT_MS = 20000; +static const uint32_t ATTEMPT_DELAY_MS = 2000; +static const uint8_t MAX_ATTEMPTS = 10; +static SemaphoreHandle_t wifiMutex = nullptr; +volatile bool wifi_task_running = false; + +void Wifi_Init() +{ + // Initialize LittleFS + if (!LittleFS.begin(true)) + { + ESP_LOGE(tag, "LittleFS mount failed"); + return; + } + + Wifi_Load_Settings("/system/wifi.json"); + + // Set Wi-Fi task to run on Core 1 + esp_wifi_set_ps(WIFI_PS_NONE); // Disable power save mode for better responsiveness + + // Set WiFi to AP+STA mode + WiFi.mode(WIFI_MODE_APSTA); + + // Configure and start AP + WiFi.softAPConfig(local_IP, gateway, subnet); + if (!WiFi.softAP(ap_ssid, ap_pass)) + { + ESP_LOGE(tag, "AP start failed"); + return; + } + + // Add CORS headers + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); + + Setup_WebServer_Handlers(webServer); + + WiFi.onEvent(onWiFiEvent); + WiFi.setHostname(mDnsName.c_str()); + webServer.begin(); + ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str()); + + StartWifiConnectTask(client_ssid, client_pass); + + // Wifi_Scan_for_Networks(); +} + +bool StartWifiConnectTask(String ssid = "", String pass = "") +{ + if (ssid.isEmpty() || pass.length() < 8) + { + ESP_LOGE(tag, "Invalid SSID or password"); + return false; + } + + if (!wifi_task_running) + { + client_ssid = ssid; + client_pass = pass; + if (Wifi_Task_Handle == NULL) + { + ESP_LOGD(tag, "Creating WiFi task"); + WifiClientConnected = false; + xTaskCreatePinnedToCore(Wifi_ConnectTask, "Wifi_Task", 1024 * 4, NULL, 1, &Wifi_Task_Handle, 0); + } + else + { + ESP_LOGD(tag, "WiFi task already running"); + } + + return true; + } + else + { + ESP_LOGE(tag, "Task already running"); + } + + return false; +} + +void Wifi_ConnectTask(void *parameter) +{ + static const char *tag = "Wifi_Task"; + wifi_task_running = true; + + if (!WifiClientConnected || client_ssid != WiFi.SSID()) + { + WifiClientConnected = false; + ESP_LOGD(tag, "Connecting to: %s", client_ssid.c_str()); + + // Disconnect and connect to new network + WiFi.disconnect(true); + WiFi.setAutoReconnect(false); + WiFi.begin(client_ssid.c_str(), client_pass.c_str()); + + // Wait for connection + uint8_t attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < MAX_ATTEMPTS) + { + switch (WiFi.status()) + { + case WL_NO_SSID_AVAIL: + ESP_LOGW(tag, "SSID not found: %s", client_ssid.c_str()); + break; + case WL_CONNECT_FAILED: + ESP_LOGW(tag, "Connection failed - check password"); + break; + default: + ESP_LOGD(tag, "Connecting... (%d/%d)", attempts + 1, MAX_ATTEMPTS); + } + vTaskDelay(pdMS_TO_TICKS(ATTEMPT_DELAY_MS)); + attempts++; + } + + // Check if connected + if (WiFi.status() == WL_CONNECTED) + { + ESP_LOGI(tag, "Connected to %s", client_ssid.c_str()); + WifiClientConnected = true; + WiFi.setAutoReconnect(true); + + if (!Wifi_Save_Credentials("/system/wifi.json")) + { + ESP_LOGW(tag, "Failed to save credentials"); + } + + Wifi_Check_Internet(); + } + else + { + ESP_LOGE(tag, "Failed to connect after %d attempts", MAX_ATTEMPTS); + } + } + + ESP_LOGI(tag, "Wifi Task ended"); + + Wifi_Task_Handle = NULL; + wifi_task_running = false; + vTaskDelete(NULL); +} + +void Wifi_Check_Internet() +{ + // Check for internet connection + const char *host = "8.8.8.8"; // Google DNS server + if (Ping.ping(host, 1)) + { + InternetAvailable = true; + ESP_LOGI(tag, "Internet connection verified"); + } + else + { + InternetAvailable = false; + ESP_LOGW(tag, "No internet connection"); + } +} + +bool Wifi_Save_Credentials(String path) +{ + // Load existing JSON + JsonDocument doc; + File readFile = LittleFS.open(path, "r"); + if (readFile) + { + DeserializationError error = deserializeJson(doc, readFile); + readFile.close(); + if (error) + { + ESP_LOGE(tag, "Failed to parse existing JSON"); + return false; + } + } + + // Update or create wifi-client section + JsonObject wifiClient = doc["wifi-client"].to(); + wifiClient["ssid"] = client_ssid; + wifiClient["pass"] = client_pass; + + // Save updated JSON + File writeFile = LittleFS.open(path, "w"); + if (!writeFile) + { + ESP_LOGE(tag, "Error opening %s for writing", path.c_str()); + return false; + } + + // Serialize JSON with pretty formatting + if (serializeJsonPretty(doc, writeFile) == 0) + { + ESP_LOGE(tag, "Failed to write JSON to file"); + writeFile.close(); + return false; + } + + writeFile.close(); + return true; +} + +void Wifi_Load_Settings(String path) +{ + // Load WiFi settings + File file = LittleFS.open(path, "r"); + if (!file) + { + ESP_LOGE(tag, "Error opening %s", path.c_str()); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) + { + ESP_LOGE(tag, "Failed to deserialize %s", path.c_str()); + return; + } + + JsonObject wifiJson = doc.as(); + if (wifiJson.isNull()) + { + ESP_LOGE(tag, "%s is empty", path.c_str()); + return; + } + + // Load AP settings + JsonObject apJson = wifiJson["wifi-ap"]; + if (!apJson.isNull()) + { + ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP"); + ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678"); + local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1")); + gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1")); + subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0")); + } + + // Load Client settings + JsonObject clientJson = wifiJson["wifi-client"]; + if (!apJson.isNull()) + { + client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none"); + client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678"); + } +} + +void Wifi_Scan_for_Networks() +{ + // Start a scan for available networks + WiFi.scanNetworks(false, false); + while (WiFi.scanComplete() == WIFI_SCAN_RUNNING) + { + vTaskDelay(100); // Wait for scan to complete + } + + networkCount = WiFi.scanComplete(); + if (networkCount >= 0) + { + JsonDocument doc; + JsonArray networks = doc["networks"].to(); + + for (int i = 0; i < networkCount; i++) + { + auto network = networks.add(); + network["ssid"] = WiFi.SSID(i); + network["rssi"] = WiFi.RSSI(i); + network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN; + } + + String jsonString; + serializeJson(doc, jsonString); + networkList = jsonString; + WiFi.scanDelete(); + } + else + { + ESP_LOGE(tag, "WiFi scan failed"); + } +} + +void Setup_WebServer_Handlers(AsyncWebServer &server) +{ + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LittleFS, "/www/index.html", "text/html"); }); + server.on("/home", HTTP_GET, [](AsyncWebServerRequest *request) + { sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); }); + server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request) + { + if (!request->authenticate(http_username, http_password)) + { + return request->requestAuthentication(); + } + // sendHtmlFile("/www/setup.html", request, htmlProcessor); + }); + server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request) + { + sendHtmlFile("/www/about.html", request, fileManagerHtmlProcessor); + // request->send(LittleFS, "/www/about.html", "text/html"); + }); + + // Wifi related handlers + server.on("/wifi/connect", HTTP_POST, [](AsyncWebServerRequest *request) + { + // Input validation + if (!request->hasParam("ssid", false, false) || !request->hasParam("pass", false, false)) { + ESP_LOGE(tag, "Missing required parameters"); + request->send(400, "application/json", "{\"error\":\"Missing ssid or password\"}"); + return; + } + + // Get and store credentials + String ssid = request->getParam("ssid", false, false)->value(); + String pass = request->getParam("pass", false, false)->value(); + + // Validate credentials + if (client_ssid.length() < 1 || client_pass.length() < 8) { + ESP_LOGE(tag, "Invalid credentials"); + request->send(400, "application/json", "{\"error\":\"Invalid credentials\"}"); + return; + } + + // Start connection + StartWifiConnectTask(ssid, pass); + + ESP_LOGD(tag, "Starting connection to %s", client_ssid.c_str()); + request->send(200, "application/json", "{\"status\":\"connecting\"}"); }); + server.on("/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request) + { + String jsonStr = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + + "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}"; + request->send(200, "application/json", jsonStr); }); + server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(networkCount <= 0) { + request->send(400, "application/json", "{\"error\":\"No scan results\"}"); + return; + } + + request->send(200, "application/json", networkList); }); + server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(WiFi.getMode() == WIFI_MODE_APSTA){ + // TODO Disable navigation bar + } + //sendHtmlFile("/www/wifi.html", request, htmlProcessor); + request->send(LittleFS, "/www/wifi.html", "text/html"); }); + + // File Manager related handlers + server.on("/files/upload", HTTP_POST, [](AsyncWebServerRequest *request) + { request->send(200); }, handleFilesUpload_OnBody); + + server.on("/files/download", HTTP_GET, [](AsyncWebServerRequest *request) + { + if (!request->hasParam("file")) { + ESP_LOGE(tag, "Missing file parameter"); + request->send(400, "text/plain", "Missing file parameter"); + return; + } + + try { + String filename = uriDecode(request->getParam("file")->value()); + ESP_LOGD(tag, "Download request for: %s", filename.c_str()); + request->send(LittleFS, filename, "application/octet-stream"); + } + catch (const std::exception& e) { + ESP_LOGE(tag, "Download failed: %s", e.what()); + request->send(404, "text/plain", "File not found!"); + } }); + server.on("/files/delete", HTTP_GET, [](AsyncWebServerRequest *request) + { + static const char* tag = "DeleteHandler"; + + // Authentication check + if (!request->authenticate(http_username, http_password)) { + ESP_LOGW(tag, "Authentication failed for delete request"); + return request->requestAuthentication(); + } + + // Parameter validation + if (!request->hasParam(param_delete_path)) { + ESP_LOGE(tag, "Missing delete path parameter"); + request->send(400, "text/plain", "Missing file path"); + return; + } + + // Get and validate filename + String filename = uriDecode(request->getParam(param_delete_path)->value()); + if (filename == "choose") { + request->redirect("/files"); + return; + } + + // Ensure path starts with / + if (!filename.startsWith("/")) { + filename = "/" + filename; + } + + try { + if (LittleFS.remove(filename.c_str())) { + ESP_LOGI(tag, "Successfully deleted file: %s", filename.c_str()); + } else { + ESP_LOGE(tag, "Failed to delete file: %s", filename.c_str()); + request->send(500, "text/plain", "Delete failed"); + return; + } + } catch (const std::exception& e) { + ESP_LOGE(tag, "Exception during delete: %s", e.what()); + request->send(500, "text/plain", "Delete failed"); + return; + } + + request->redirect("/files"); }); + server.on("/files/edit", HTTP_GET, [](AsyncWebServerRequest *request) + { + static const char* tag = "EditHandler"; + + // Authentication check + if (!request->authenticate(http_username, http_password)) { + ESP_LOGW(tag, "Authentication failed"); + return request->requestAuthentication(); + } + + // Parameter validation + if (!request->hasParam(param_edit_path)) { + ESP_LOGE(tag, "Missing edit path parameter"); + request->send(400, "text/plain", "Missing file path"); + return; + } + + // Get and decode filename + String fileName = uriDecode(request->getParam(param_edit_path)->value()); + ESP_LOGD(tag, "Edit request for file: %s", fileName.c_str()); + + // Set save path + savePath = (fileName == "new") ? "/new.txt" : fileName; + + // Validate path + if (!savePath.startsWith("/")) { + savePath = "/" + savePath; + } + + ESP_LOGD(tag, "Save path set to: %s", savePath.c_str()); + + try { + sendHtmlFile("/www/edit.html", request, fileManagerHtmlProcessor); + } catch (const std::exception& e) { + ESP_LOGE(tag, "Failed to send edit page: %s", e.what()); + request->send(500, "text/plain", "Internal server error"); + } }); + server.on("/files/save", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + String inputMessage((char*)0); + if (request->hasParam(param_edit_textarea)) { + inputMessage = request->getParam(param_edit_textarea)->value(); + } + if (request->hasParam(param_save_path)) { + savePath = uriDecode(request->getParam(param_save_path)->value()); + } + writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); + + request->redirect("/files"); }); + server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/files.html", request, fileManagerHtmlProcessor); }); + + // Lights related handlers + server.on("/lights/settings", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(200); }); + server.on("/lights/settings", HTTP_POST, [](AsyncWebServerRequest *request) + { request->send(200); }); + server.on("/lights/animation", HTTP_POST, [](AsyncWebServerRequest *request) + { request->send(200); }); + server.on("/lights/setpixel", HTTP_POST, [](AsyncWebServerRequest *request) + { request->send(200); }); + + // System and LED related handlers + server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request) + { + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request) + { + /* + //CreateSysSummmaryPacket(doc); + String summary; + serializeJson(jsDoc, summary); + request->send(200, "application/json", summary); + */ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request) + { + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + + // LightStik related handlers + server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request) + { + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request) + { + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request) + { + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); }); + + // Firmware Update Handlers + server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request) + { + //String newVersion; + loadUpdateJson(); + //bool avai = checkManifest(FIRMWARE_VERSION, newVersion); + checkManifest(otaVersion); + bool avail = otaVersion > localVersion; + + JsonDocument doc; + doc["currentVersion"] = localVersion.toString(); + doc["latestVersion"] = otaVersion.toString(); + doc["updateAvailable"] = avail; + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); }); + // Start update process + server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request) + { + startFirmwareUpdateTask(&eventUpgradeProgress); + request->send(200); }); + eventUpgradeProgress.onConnect([](AsyncEventSourceClient *client) + { + if (client->lastId()) + { + Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId()); + } + // send event with message "hello!", id current millis + // and set reconnect delay to 1 second + // client->send("hello!", NULL, millis(), 10000); + }); + server.on("/upgrade", HTTP_GET, [](AsyncWebServerRequest *request) + { + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + request->send(LittleFS, "/www/upgrade.html", "text/html"); }); + server.addHandler(&eventUpgradeProgress); + + // Basic Connection status check + server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(200, "application/json", "{\"status\":\"connected\"}"); }); + // Server requested files that aren't template processed + server.on("/*", HTTP_GET, [](AsyncWebServerRequest *request) { // handle file uploads + // Validate request + if (!request) + { + ESP_LOGE(tag, "Invalid request"); + return; + } + + // Get and validate file path + String filePath = request->url(); + if (filePath.isEmpty()) + { + ESP_LOGE(tag, "Empty file path"); + request->send(400, "text/plain", "Invalid file path"); + return; + } + + // Ensure path starts with '/' + if (!filePath.startsWith("/")) + { + filePath = "/" + filePath; + } + + try + { + // Get content type once + const char *contentType = getFileType(getFileExtension(filePath.c_str())); + if (!contentType) + { + ESP_LOGW(tag, "Unknown file type: %s", filePath.c_str()); + contentType = "application/octet-stream"; + } + + ESP_LOGD(tag, "Sending file: %s (%s)", filePath.c_str(), contentType); + request->send(LittleFS, filePath, contentType); + } + catch (const std::runtime_error &e) + { + ESP_LOGE(tag, "FileSystem error: %s for path: %s", e.what(), filePath.c_str()); + request->send(404, "text/plain", "File not found"); + } + catch (const std::exception &e) + { + ESP_LOGE(tag, "Error: %s for path: %s", e.what(), filePath.c_str()); + request->send(500, "text/plain", "Internal server error"); + } + }); + // 404 handler + server.onNotFound([](AsyncWebServerRequest *request) + { + ESP_LOGE(tag, "404: %s", request->url().c_str()); + request->send(404, "text/plain", "Not found"); }); +} + +void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit + + if (!index) + { + // Initial upload chunk + if (!request->hasParam("dir-path", true, false)) + { + ESP_LOGE(tag, "Missing dir-path parameter"); + request->send(400, "text/plain", "Missing dir-path"); + return; + } + + AsyncWebParameter *p = request->getParam("dir-path", true, false); + String path = p->value() + "/" + filename; + ESP_LOGD(tag, "Starting upload: %s", path.c_str()); + + // Validate path + if (!path.startsWith("/")) + { + path = "/" + path; + } + + request->_tempFile = LittleFS.open(path, "w"); + if (!request->_tempFile) + { + ESP_LOGE(tag, "Failed to create file: %s", path.c_str()); + request->send(500, "text/plain", "Failed to create file"); + return; + } + } + + // Write chunk + if (len && request->_tempFile) + { + if (index + len > MAX_UPLOAD_SIZE) + { + request->_tempFile.close(); + LittleFS.remove(request->_tempFile.name()); + ESP_LOGE(tag, "Upload too large"); + request->send(413, "text/plain", "File too large"); + return; + } + + if (!request->_tempFile.write(data, len)) + { + ESP_LOGE(tag, "Write failed"); + request->_tempFile.close(); + request->send(500, "text/plain", "Write failed"); + return; + } + } + + if (final) + { + request->_tempFile.close(); + ESP_LOGD(tag, "Upload complete: %s, %u bytes", filename.c_str(), index + len); + request->redirect("/files"); + } +} + +// Send html file with template processing {{VAR}} +void sendHtmlFile(const char *filePath, AsyncWebServerRequest *request, String (*callback)(const String &)) +{ + try + { + const char *htmlFile = readFile(LittleFS, filePath); + if (!htmlFile) + { + ESP_LOGE(tag, "Failed to read file: %s", filePath); + request->send(404, "text/plain", "File not found"); + return; + } + + String processedData = varReplace(htmlFile, callback); + delete[] htmlFile; // Clean up allocated memory + + ESP_LOGD(tag, "Sent file: %s", filePath); + request->send(200, "text/html", processedData); + } + catch (const std::exception &e) + { + ESP_LOGE(tag, "Error processing file %s: %s", filePath, e.what()); + request->send(500, "text/plain", "Server error"); + } +} + +const char *getFileExtension(const char *filename) +{ + // Input validation + if (!filename) + { + ESP_LOGW(tag, "Null filename provided"); + return ""; + } + + // Find last dot + const char *lastDot = strrchr(filename, '.'); + if (!lastDot || lastDot == filename || *(lastDot + 1) == '\0') + { + ESP_LOGD(tag, "No valid extension found in: %s", filename); + return ""; + } + + ESP_LOGD(tag, "Found extension: %s", lastDot + 1); + return lastDot + 1; +} + +const char *getFileType(const char *ext) +{ + if (!ext) + return "application/octet-stream"; + + ESP_LOGD(tag, "Getting file type for extension: %s", ext); + + if (strcmp(ext, "png") == 0) + return "image/png"; + if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) + return "image/jpeg"; + if (strcmp(ext, "gif") == 0) + return "image/gif"; + if (strcmp(ext, "ico") == 0) + return "image/x-icon"; + if (strcmp(ext, "txt") == 0) + return "text/plain"; + if (strcmp(ext, "css") == 0) + return "text/css"; + if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) + return "text/html"; + if (strcmp(ext, "js") == 0) + return "text/javascript"; + if (strcmp(ext, "json") == 0) + return "application/json"; + + ESP_LOGW(tag, "Unknown file extension: %s", ext); + return "application/octet-stream"; +} + +// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content +String varReplace(const String &input, String (*callback)(const String &)) +{ + static const char *tag = "varReplace"; + + // Validate inputs + if (input.isEmpty() || !callback) + { + ESP_LOGW(tag, "Empty input or null callback"); + return input; + } + + // Pre-allocate result string with estimated size + String result; + result.reserve(input.length() * 1.2); // Add 20% for potential replacements + + const int maxSegmentLength = 32; + int startPos = 0; + + // Process all segments + while (true) + { + // Find next variable + int start = input.indexOf("{{", startPos); + if (start == -1) + { + break; + } + + // Add text before variable + result += input.substring(startPos, start); + + // Find end of variable + int end = input.indexOf("}}", start + 2); + if (end == -1) + { + ESP_LOGW(tag, "Unmatched {{ at position %d", start); + result += input.substring(start); + break; + } + + // Extract and validate segment + String segment = input.substring(start + 2, end); + if (segment.length() <= maxSegmentLength) + { + try + { + String replacement = callback(segment); + result += replacement; + } + catch (const std::exception &e) + { + ESP_LOGE(tag, "Callback error: %s", e.what()); + result += input.substring(start, end + 2); + } + } + else + { + ESP_LOGW(tag, "Segment too long: %d chars", segment.length()); + result += input.substring(start, end + 2); + } + + startPos = end + 2; + } + + // Add remaining text + if (startPos < input.length()) + { + result += input.substring(startPos); + } + + return result; +} + +const char *convertFileSize(const size_t bytes) +{ + static char fileSizeBuffer[16]; // Pre-allocated buffer for the file size + + if (bytes < 1024) + { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes); + } + else if (bytes < 1024 * 1024) + { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", bytes / 1024.0); + } + else if (bytes < 1024 * 1024 * 1024) + { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", bytes / (1024.0 * 1024.0)); + } + else + { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } + + return fileSizeBuffer; +} + +void onWiFiEvent(WiFiEvent_t event) +{ + // Serial.printf("[WiFi-event] event: %d\n", event); + switch (event) + { + case ARDUINO_EVENT_WIFI_READY: + ESP_LOGD(tag, "WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + ESP_LOGD(tag, "Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + ESP_LOGD(tag, "WiFi client started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + ESP_LOGD(tag, "WiFi clients stopped"); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + ESP_LOGD(tag, "Connected to AP"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + WifiClientConnected = false; + ESP_LOGD(tag, "WiFi Disconnected"); + setStatusPin1(false); + Buzzer_Play_Tune(TUNE_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + ESP_LOGD(tag, "Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + WifiClientConnected = true; + ESP_LOGD(tag, "My IP: %s", WiFi.localIP().toString()); + // Wifi_Start_MDNS(); + setStatusPin1(true); + Buzzer_Play_Tune(TUNE_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + WifiClientConnected = false; + ESP_LOGD(tag, "Lost IP address and IP address is reset to 0"); + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + ESP_LOGD(tag, "WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + ESP_LOGD(tag, "WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + ESP_LOGD(tag, "WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + ESP_LOGD(tag, "WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + ESP_LOGD(tag, "WiFi access point started"); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + ESP_LOGD(tag, "WiFi access point stopped"); + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + ESP_LOGD(tag, "Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + ESP_LOGD(tag, "SoftAP Client Disconnected"); + Buzzer_Play_Tune(TUNE_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + ESP_LOGD(tag, "SoftAP Client Connected"); + Buzzer_Play_Tune(TUNE_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + ESP_LOGD(tag, "Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + ESP_LOGD(tag, "AP IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: + ESP_LOGD(tag, "STA IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_GOT_IP6: + ESP_LOGD(tag, "Ethernet IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_START: + ESP_LOGD(tag, "Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + ESP_LOGD(tag, "Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + ESP_LOGD(tag, "Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + ESP_LOGD(tag, "Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + ESP_LOGD(tag, "Obtained IP address"); + break; + default: + break; + } +} + +void Wifi_Start_MDNS(void) +{ + ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str()); + if (!MDNS.begin(mDnsName.c_str())) + { + ESP_LOGE(tag, "Error setting up MDNS responder!"); + } + else + { + ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName); + } +} + +bool writeFile(fs::FS &fs, const char *path, const char *message) +{ + // Validate inputs + if (!path || !message) + { + ESP_LOGE(tag, "Invalid parameters: path=%p message=%p", path, message); + return false; + } + + // Open file with error checking + File file = fs.open(path, "w"); + if (!file) + { + ESP_LOGE(tag, "Failed to open file: %s", path); + return false; + } + + // Write with error handling + try + { + size_t bytesWritten = file.print(message); + if (bytesWritten == 0) + { + ESP_LOGE(tag, "Failed to write to file: %s", path); + file.close(); + return false; + } + + // Ensure all data is written + file.flush(); + file.close(); + ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path); + return true; + } + catch (const std::exception &e) + { + ESP_LOGE(tag, "Exception while writing file %s: %s", path, e.what()); + file.close(); + return false; + } +} + +char *readFile(fs::FS &fs, const char *path) +{ + static const char *tag = "readFile"; + static const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB limit + + // Validate input + if (!path) + { + ESP_LOGE(tag, "Invalid path parameter"); + return nullptr; + } + + // Open file + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) + { + ESP_LOGE(tag, "Failed to open file: %s", path); + return nullptr; + } + + // Check file size + size_t fileSize = file.size(); + if (fileSize == 0 || fileSize > MAX_FILE_SIZE) + { + ESP_LOGE(tag, "Invalid file size: %u bytes", fileSize); + file.close(); + return nullptr; + } + + // Allocate memory + char *fileContent = new (std::nothrow) char[fileSize + 1]; + if (!fileContent) + { + ESP_LOGE(tag, "Memory allocation failed for size: %u", fileSize + 1); + file.close(); + return nullptr; + } + + // Read file + size_t bytesRead = file.readBytes(fileContent, fileSize); + file.close(); + + if (bytesRead != fileSize) + { + ESP_LOGE(tag, "Read failed: expected %u bytes, got %u", fileSize, bytesRead); + delete[] fileContent; + return nullptr; + } + + // Null terminate + fileContent[bytesRead] = '\0'; + ESP_LOGD(tag, "Successfully read %u bytes from %s", bytesRead, path); + return fileContent; +} + +String getSoftAPMacAddress() +{ + uint8_t mac[6]; + WiFi.softAPmacAddress(mac); + + String macString = ""; + for (int i = 0; i < 6; i++) + { + macString += String(mac[i], HEX); + if (i < 5) + macString += ":"; + } + return macString; +} + +String listDirAsHtml(String directoryList[], int count) +{ + String listedFiles; + + for (int i = 0; i < count; i++) + { + // directory html + listedFiles += "Dir: "; + listedFiles += directoryList[i]; + listedFiles += "/-\n"; + + filesDropdownOptions += "\n"; + + dirDropdownOptions += "\n"; + + File dir = LittleFS.open(directoryList[i]); + File file = dir.openNextFile(); + while (file) + { + String fileName = file.name(); + if (!file.isDirectory()) + { + // Serial.println(" File: " + String(file.name())); + listedFiles += "  "; + listedFiles += fileName; + listedFiles += ""; + listedFiles += convertFileSize(file.size()); + listedFiles += "\n"; + + filesDropdownOptions += "\n"; + } + file = dir.openNextFile(); + } + dir.close(); + } + + return listedFiles; +} + +/******************** Specific Html Processors ********************/ +// file manager html processor +String fileManagerHtmlProcessor(const String &var) +{ + if (var == "ALLOWED_EXTENSIONS_EDIT") + { + return allowedExtensionsForEdit; + } + + if (var == "FS_FREE_BYTES") + { + return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); + } + + if (var == "FS_USED_BYTES") + { + return convertFileSize(LittleFS.usedBytes()); + } + + if (var == "FS_TOTAL_BYTES") + { + return convertFileSize(LittleFS.totalBytes()); + } + + if (var == "RAM_FREE_BYTES") + { + return convertFileSize(LittleFS.totalBytes()); + } + + if (var == "RAM_USED_BYTES") + { + return convertFileSize(LittleFS.totalBytes()); + } + + if (var == "RAM_TOTAL_BYTES") + { + return convertFileSize(LittleFS.totalBytes()); + } + + if (var == "LISTED_FILES") + { + filesDropdownOptions = ""; // clear out + dirDropdownOptions = ""; // clear out + String directories[MAX_DIRECTORIES]; + int dirCount = 0; + getAllDirectories(directories, dirCount); + return listDirAsHtml(directories, dirCount); + } + + if (var == "EDIT-DEL_FILES") + { + return filesDropdownOptions; + } + + if (var == "DIR_LIST") + { + return dirDropdownOptions; + } + + if (var == "SAVE_PATH_INPUT") + { + return savePath; + } + + if (var == "FIRM_VER") + { + return localVersion.toString(); + } + + return var; +} + +String HomeHtmlProcessor(const String &var) +{ + if (var == "APP_NAME") + { + // return sysProps.appName; + return "N/A"; + } + if (var == "OLED") + { + return "N/A"; + } + if (var == "STRIP1") + { + // return (strip1) ? "Yes" : "No"; + return "N/A"; + } + if (var == "STRIP2") + { + // return (strip2) ? "Yes" : "No"; + return "N/A"; + } + if (var == "FRONT_LIGHT") + { + // return (animProps.frontLight.enabled) ? "Yes" : "No"; + return "N/A"; + } + if (var == "REAR_LIGHT") + { + // return (animProps.rearLight.enabled) ? "Yes" : "No"; + return "N/A"; + } + if (var == "FIRMWARE") + { + return localVersion.toString(); + } + if (var == "BOOTH_T") + { + // return String(sysProps.t_sensor.temperature) + "F"; + return "N/A"; + } + if (var == "SETPOINT") + { + // return String(sysProps.t_sensor.Setpoint1) + "F"; + return "N/A"; + } + if (var == "FLASH_SIZE") + { + return convertFileSize(ESP.getSketchSize()); + } + if (var == "FLASH_FREE") + { + return convertFileSize(ESP.getFreeSketchSpace()); + } + if (var == "HEAP_SIZE") + { + return convertFileSize(ESP.getHeapSize()); + } + if (var == "HEAP_FREE") + { + return convertFileSize(ESP.getFreeHeap()); + } + if (var == "CPU_FREQ") + { + return String(ESP.getCpuFreqMHz()) + "Mhz"; + } + if (var == "IP") + { + return WiFi.localIP().toString(); + } + if (var == "MAC") + { + return chipInfo.macStr; + } + if (var == "SSID") + { + return WiFi.SSID(); + } + if (var == "RSSI") + { + return String(WiFi.RSSI()); + } + if (var == "WIFI_CH") + { + return String(WiFi.channel()); + } + if (var == "ENCRYP") + { + return String(WiFi.encryptionType(0)); + } + if (var == "AP_SSID") + { + return WiFi.softAPSSID(); + } + if (var == "AP_CLIENTS") + { + return String(WiFi.softAPgetStationNum()); + } + if (var == "BLE") + { + return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No"; + } + if (var == "BLE_SSID") + { + // return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : ""; + return "N/A"; + } + if (var == "BLE_CLIENTS") + { + // return (BTDeviceConnected) ? "1" : "0"; + return "N/A"; + } + if (var == "AP_MAC") + { + return getSoftAPMacAddress(); + } + + // Return an empty string if the variable is not recognized + return var; +} + +void handleUpdateProgress(AsyncWebServerRequest *request) +{ + static const char *tag = "UpdateProgress"; + + // if (!request->authenticate(http_username, http_password)) { + // return request->requestAuthentication(); + // } + + request->send(200, "text/plain", "Update progress"); // Send a simple response +} diff --git a/temporary/BLE-FlashStick-Service.h b/temporary/BLE-FlashStick-Service.h new file mode 100644 index 0000000..29260e8 --- /dev/null +++ b/temporary/BLE-FlashStick-Service.h @@ -0,0 +1,4 @@ +#pragma once + +#include +#include diff --git a/temporary/BLE-FlaskStick-Service.cpp b/temporary/BLE-FlaskStick-Service.cpp new file mode 100644 index 0000000..2172ffc --- /dev/null +++ b/temporary/BLE-FlaskStick-Service.cpp @@ -0,0 +1,157 @@ +#include "BLE-FlashStick-Service.h" +#include "WiFi.h" +#include "my_wifi.h" +#include "global.h" +#include "AppUpgrade.h" +#include "AppVersion.h" + +static const char *tag = "BLE_FlashStickService"; + +#define UPGRADE_SERVICE_UUID "abcdef03-2345-6789-1234-56789abcdef0" +#define UPGRADE_CHARACTERISTIC1_UUID "abcdef03-2345-6789-1234-56789abcdef1" + +NimBLEService *pUpgradeService = nullptr; +NimBLECharacteristic *pUpgradeCharacteristic1 = nullptr; + +enum WIFI_STAT : byte { WIFI_DISCONNECTED=0, WIFI_BAD_CREDS=1, WIFI_NO_AP=2, WIFI_CONNECTED=3 }; + +struct FLASHSTICK_PACKET { + bool reistered = false; + char msg[16] = "Hello..."; +}flashstickPacket; + + +// Class for handling server events +class ServerCallbacks : public NimBLEServerCallbacks { + void onConnect(NimBLEServer* pServer) override { + ESP_LOGI(tag, "Flash-Stick connected"); + } + + void onDisconnect(NimBLEServer* pServer) override { + ESP_LOGI(tag, "Flash-Stick disconnected"); + } +}; + + +// Class for handling characteristic events +class UpgradeChar_Callbacks : public NimBLECharacteristicCallbacks { + + void onWrite(NimBLECharacteristic *pCharacteristic) override { + std::string value = pCharacteristic->getValue(); + ESP_LOGD(tag, "Upgrade Char written with value: %s", value.c_str()); + + if (value.compare(0, 12, "wifi-connect") == 0) { // Update WiFi credentials + JsonDocument doc; + deserializeJson(doc, value.substr(13)); + JsonObject wifiJson = doc.as(); + String ssid = wifiJson["ssid"].as(); + String pass = wifiJson["pass"].as(); + ESP_LOGI(tag, "Wifi Credentials: %s, %s", ssid.c_str(), pass.c_str()); + + bool status = StartWifiConnectTask(ssid, pass); + if(status == true){ + updatePacket.wifiStatus = WIFI_DISCONNECTED; + updatePacket.wifiOnline = false; + updatePacket.wifiIP[0] = updatePacket.wifiIP[1] = updatePacket.wifiIP[2] = updatePacket.wifiIP[3] = 0; + }else{ + ESP_LOGI(tag, "Failed to start WiFi connection task"); + } + } + else if (value.compare("version-check") == 0) { // Check if new version is available + ESP_LOGI(tag, "Version check command received: newVersion=%d.%d.%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch()); + if(updatePacket.newVersion[0] == 0){ + startVersionCheckTask(); // start the task and done + }else{ + ESP_LOGI(tag, "Version already checked"); + } + } + else if (value.compare("upgrade-start") == 0) { // Start OTA update + ESP_LOGI(tag, "Start OTA update command received"); + startFirmwareUpdateTask(nullptr); // start the task + } + else if (value.compare("rename-device") == 0) { // Start renaming device + ESP_LOGI(tag, "Start renane device command received"); + } + else { + ESP_LOGW(tag, "Unknown command received: %s", value.c_str()); + } + } + + void onRead(NimBLECharacteristic *pCharacteristic) override { + updatePacket.wifiOnline = InternetAvailable; + if(WiFi.status() == WL_CONNECTED){ + updatePacket.wifiStatus = WIFI_CONNECTED; + if(updatePacket.wifiIP[0] == 0){ + updatePacket.wifiIP[0] = WiFi.localIP()[0]; + updatePacket.wifiIP[1] = WiFi.localIP()[1]; + updatePacket.wifiIP[2] = WiFi.localIP()[2]; + updatePacket.wifiIP[3] = WiFi.localIP()[3]; + } + }else{ + updatePacket.wifiStatus = WIFI_DISCONNECTED; + if(updatePacket.wifiIP[0] > 0){ + updatePacket.wifiIP[0] = 0; + updatePacket.wifiIP[1] = 0; + updatePacket.wifiIP[2] = 0; + updatePacket.wifiIP[3] = 0; + } + } + + //update version + if(otaVersion.major() != 0){ + ESP_LOGI(tag, "Updated new version: major=%d, minor=%d, patch=%d", otaVersion.major(), otaVersion.minor(), otaVersion.patch()); + updatePacket.newVersion[0] = otaVersion.major(); + updatePacket.newVersion[1] = otaVersion.minor(); + updatePacket.newVersion[2] = otaVersion.patch(); + } + + pCharacteristic->setValue(reinterpret_cast(&updatePacket), sizeof(updatePacket)); + ESP_LOGI(tag, "Upgrade Char read"); + } +}; + + +void bleUpgrade_send_message(String s){ + if(pUpgradeCharacteristic2){ + if (s != nullptr) { + pUpgradeCharacteristic2->setValue(s); + pUpgradeCharacteristic2->notify(); + } else { + ESP_LOGW(tag, "Null string passed to bleUpgrade_send_message"); + } + } +} + + +void Init_UpgradeBLEService(NimBLEServer *pServer){ + + // Create Upgrade BLE Service + pUpgradeService= pServer->createService( UPGRADE_SERVICE_UUID ); + + pUpgradeCharacteristic1 = pUpgradeService->createCharacteristic( + UPGRADE_CHARACTERISTIC1_UUID, + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pUpgradeCharacteristic1->setCallbacks(new UpgradeChar_Callbacks()); + ESP_LOGI(tag, "Upgrade callback registered!"); + + + pUpgradeCharacteristic2 = pUpgradeService->createCharacteristic( + UPGRADE_CHARACTERISTIC2_UUID, + NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY + ); + + // Register the callback with the characteristic + pUpgradeCharacteristic2->setCallbacks(new UpgradeChar_Callbacks()); + ESP_LOGI(tag, "Upgrade callback registered!"); + + pUpgradeService->start(); + + NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising(); + pAdvertising->addServiceUUID( UPGRADE_SERVICE_UUID ); // Advertise service UUID + + +} + diff --git a/temporary/EventBoxTest.html b/temporary/EventBoxTest.html new file mode 100644 index 0000000..114f15e --- /dev/null +++ b/temporary/EventBoxTest.html @@ -0,0 +1,82 @@ + + + + + + + +Event Container + + + +
+
+
+ + + +
+
+ + + + diff --git a/temporary/Temp/ATALights2.cpp b/temporary/Temp/ATALights2.cpp new file mode 100644 index 0000000..d64a258 --- /dev/null +++ b/temporary/Temp/ATALights2.cpp @@ -0,0 +1,178 @@ +#include "ATALights.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include +#include "Animations.h" +#include +#include + + +static const char* tag = "strips"; + +// Define constants for maximum LEDs +#define MAX_LEDS 300 // Adjust based on your LED requirements + +// Create pointers for NeoPixelBus objects +void* strip1 = nullptr; +void* strip2 = nullptr; + +TaskHandle_t Animation_Task_Handle; + +// Runtime configuration variables +int numLeds1 = 0, numLeds2 = 0; // Number of LEDs for each strip +int dataPin1 = -1, dataPin2 = -1; // Data pins for each strip +String chipType1, chipType2; // Chip types (e.g., WS2812, SK6812, TM1814) +String colorOrder1, colorOrder2; // Color orders (e.g., GRB, RGB, BGR) + + +void Init_Lights_Task(void){ + + xTaskCreatePinnedToCore(Lights_Control_Task, "LumaMaster_Task", 1024*6, NULL, 1, &Animation_Task_Handle, CONFIG_ARDUINO_RUNNING_CORE); + ESP_LOGI(tag, "Lights Task Created..."); + + + // Example runtime configuration for two strips + dataPin1 = 5; // Pin for Strip 1 + numLeds1 = 150; // Number of LEDs on Strip 1 + chipType1 = "WS2812"; // Chip type for Strip 1 + colorOrder1 = "GRB"; // Color order for Strip 1 + + dataPin2 = 18; // Pin for Strip 2 + numLeds2 = 100; // Number of LEDs on Strip 2 + chipType2 = "WS2812"; // Chip type for Strip 2 + colorOrder2 = "RGB"; // Color order for Strip 2 + + // Dynamically initialize the strips + strip1 = initializeStrip(dataPin1, numLeds1, chipType1, colorOrder1); + strip2 = initializeStrip(dataPin2, numLeds2, chipType2, colorOrder2); + + + // Start the strips if initialized + if (strip1) static_cast*>(strip1)->Begin(); + if (strip2) static_cast*>(strip2)->Begin(); +} + + +void Animation_Loop_Exit(void){ + if( Animation_Task_Handle ){ + xTaskNotifyGive( Animation_Task_Handle ); + } +} + + +void LightsON(void){ + FastLED.show(); +} + + +void LightsOff(void){ + FastLED.clear(); + FastLED.show(); +} + + + +inline void setPixel1(int pixelIndex, const CRGB col) { + register uint16_t x = pixelIndex + ledSettings[0].shift; + // If strip.effSize is power of 2, use faster bit masking + if ((ledSettings[0].effSize & (ledSettings[0].effSize - 1)) == 0) { + x = (x < 0) ? ((x + ledSettings[0].effSize) & (ledSettings[0].effSize - 1)) : (x & (ledSettings[0].effSize - 1)); + leds1[(x + ledSettings[0].offset) & (ledSettings[0].effSize - 1)] = col; + } else { + // For non-power-of-2 sizes, still need modulo + x = (x < 0) ? ((x + ledSettings[0].effSize) % ledSettings[0].effSize) : (x % ledSettings[0].effSize); + leds1[(x + ledSettings[0].offset) % ledSettings[0].effSize] = col; + } +} + + +inline void setPixel2(int pixelIndex, const CRGB col) { + register uint16_t x = pixelIndex + ledSettings[1].shift; + // If strip.effSize is power of 2, use faster bit masking + if ((ledSettings[1].effSize & (ledSettings[1].effSize - 1)) == 0) { + x = (x < 0) ? ((x + ledSettings[1].effSize) & (ledSettings[1].effSize - 1)) : (x & (ledSettings[1].effSize - 1)); + leds2[(x + ledSettings[1].offset) & (ledSettings[1].effSize - 1)] = col; + } else { + // For non-power-of-2 sizes, still need modulo + x = (x < 0) ? ((x + ledSettings[1].effSize) % ledSettings[1].effSize) : (x % ledSettings[1].effSize); + leds2[(x + ledSettings[1].offset) % ledSettings[1].effSize] = col; + } +} + + +void Lights_Control_Task_Resume(void){ + vTaskResume(Animation_Task_Handle); +} + +void Lights_Control_Task(void *parameters){ + + ESP_LOGD(tag, "Lights Control Task Entered..."); + vTaskSuspend(NULL); + ESP_LOGD(tag, "Lights Control Task Resumed..."); + vTaskDelay(2000); + fill_solid(leds1, ledSettings[0].size-1, CRGB::Blue); + FastLED.show(); + vTaskDelay(5000); + + while(true){ + Animation_Loop(2000, [&]() { + ESP_LOGD(tag, "Looping...."); + + // Example animation: Alternate colors between strips + setStripColor>(strip1, numLeds1, RgbColor(255, 0, 0)); // Red for Strip 1 + setStripColor>(strip2, numLeds2, RgbColor(0, 255, 0)); // Green for Strip 2 + delay(1000); + + setStripColor>(strip1, numLeds1, RgbColor(0, 0, 255)); // Blue for Strip 1 + setStripColor>(strip2, numLeds2, RgbColor(255, 255, 0)); // Yellow for Strip 2 + delay(1000); + }); + + /* + uint8_t animMode = 1; + switch(animMode){ + case 0: + break; + case 1: + break; + } + */ + } +} + + +void Init_FastLED_Strip(CRGB* leds, uint8_t pin, int size, EOrder rgbOrder, const String& chipType) { + + + +} + +// Function to initialize a strip dynamically (Non-SPI chipsets only) +void* initializeStrip(int dataPin, int numLeds, const String& chipType, const String& colorOrder) { + if (chipType == "WS2812" || chipType == "SK6812") { + if (colorOrder == "GRB") { + return new NeoPixelBus(numLeds, dataPin); + } else if (colorOrder == "RGB") { + return new NeoPixelBus(numLeds, dataPin); + } else if (colorOrder == "BGR") { + return new NeoPixelBus(numLeds, dataPin); + } + } + Serial.println("Unsupported chipset or color order!"); + return nullptr; +} + +// Function to set all LEDs of a strip to a specific color +template +void setStripColor(void* strip, int numLeds, RgbColor color) { + if (strip) { + T* actualStrip = static_cast(strip); + for (int i = 0; i < numLeds; i++) { + actualStrip->SetPixelColor(i, color); + } + actualStrip->Show(); + } +} + + diff --git a/temporary/Temp/BTSerial.cpp b/temporary/Temp/BTSerial.cpp new file mode 100644 index 0000000..4295c4b --- /dev/null +++ b/temporary/Temp/BTSerial.cpp @@ -0,0 +1,240 @@ +#include "BTSerial.h" +#include +#include +#include +#include +#include +#include +#include "command_processor.h" +#include "led_strip.h" +#include "global.h" +#include "JsonConstrain.h" +#include "my_buzzer.h" +#include "common/led_animation.h" + + +static const char* tag = "ble"; +TaskHandle_t BTSerial_Task_Handle; +bool BTDeviceConnected = false; + +BLEServer *pServer = NULL; +BLECharacteristic * pTxCharacteristic; +BLECharacteristic * pRxCharacteristic; +bool oldDeviceConnected = false; +uint8_t txValue = 0; + +// See the following for generating UUIDs: +// https://www.uuidgenerator.net/ +#define SERVICE_UUID_DEF "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" // UART service UUID +#define CHARACTERISTIC_UUID_RX_DEF "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define CHARACTERISTIC_UUID_TX_DEF "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" + +String BLEDeviceName; +String BLEKey; +String SERVICE_UUID; +String CHARACTERISTIC_UUID_RX; +String CHARACTERISTIC_UUID_TX; + +#define replyActive true + + +void Init_BTSerial(void) +{ + File file = LittleFS.open("/cfg/ble.json"); + if(!file){ + ESP_LOGE(tag, "Error opening ble.json..."); + } + else{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + if(error){ ESP_LOGE(tag, "ble.json deserialize error!.."); return;} + + JsonObject bleJson = doc.as(); + + // if(jsonConstrainBool(bleJson, "en", false)){ + SERVICE_UUID = jsonConstrainString(tag, bleJson, "service-uuid", SERVICE_UUID_DEF); + ESP_LOGD(tag, "SERVICE_UUID: %s", SERVICE_UUID.c_str()); + + CHARACTERISTIC_UUID_RX = jsonConstrainString(tag, bleJson, "char-uuid-rx", CHARACTERISTIC_UUID_RX_DEF); + ESP_LOGD(tag, "Char UUID RX: %s", CHARACTERISTIC_UUID_RX.c_str()); + + CHARACTERISTIC_UUID_TX = jsonConstrainString(tag, bleJson, "char-uuid-tx", CHARACTERISTIC_UUID_TX_DEF); + ESP_LOGD(tag, "Char UUID TX: %s", CHARACTERISTIC_UUID_TX.c_str()); + + BLEDeviceName = jsonConstrainString(tag, bleJson, "device-name", "ATA_COMM"); + String hexStr = String(chipInfo.macByte[0], HEX); + hexStr.toUpperCase(); + BLEDeviceName += '_'; + BLEDeviceName += hexStr; + + BLEKey = jsonConstrainString(tag, bleJson, "key", "123456"); + int core = jsonConstrain(tag, bleJson, "core", 0, 1, 0); + ESP_LOGD(tag, "BLE SSID: %s, key: %s, core: %d", BLEDeviceName.c_str(),BLEKey.c_str(), core); + + xTaskCreatePinnedToCore(BTSerial_Task, "BTSerial_Task", 12000, NULL, 1, &BTSerial_Task_Handle, core); + //} + } +} + +/** None of these are required as they will be handled by the library with defaults. ** + ** Remove as you see fit for your needs */ +class MyServerCallbacks: public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + BTDeviceConnected = true; + //BLEDevice::startAdvertising();//adding this line allows for multiple simultaneous BLE connections + }; + /* + void onConnect(BLEServer* pServer, BLEClient* pClient) { + BTDeviceConnected = true; + BLEAddress connectedAddress = pClient->getPeerAddress(); + Log.traceln("Client connected: %s", connectedAddress.toString().c_str()); + } + */ + + void onDisconnect(BLEServer* pServer) { + BTDeviceConnected = false; + } + /***************** New - Security handled here ******************** + ****** Note: these are the same return values as defaults ********/ + uint32_t onPassKeyRequest(){ + ESP_LOGD(tag, "Server PassKeyRequest"); + return 123456; + } + + bool onConfirmPIN(uint32_t pass_key){ + ESP_LOGD(tag, "The passkey YES/NO number: %d", pass_key); + return true; + } + + void onAuthenticationComplete(ble_gap_conn_desc desc){ + ESP_LOGD(tag, "Starting BLE work!"); + } + /*******************************************************************/ +}; + +#define MAX_PACKET_PARAMS 8 +int packet_data[MAX_PACKET_PARAMS]; +int paramCount = 0; +class MyCallbacks: public BLECharacteristicCallbacks +{ + void onWrite(BLECharacteristic *pCharacteristic) + { + std::string rxValue = pCharacteristic->getValue(); + + ESP_LOGD(tag, "raw: %s", rxValue.c_str()); + if(!rxValue.empty() && ((rxValue[0] == '$') || rxValue[0] == '%')){ + extractCommand(&rxValue[0], packet_data, ¶mCount); // delimited command + + // Call Animation Index Update + if(packet_data[0] == 100){ + int cntDown = 0; + if(packet_data[2]){ + cntDown = packet_data[2]; + } + animProps.event[packet_data[1]].countDown = cntDown; + animProps.event[packet_data[1]].type = EV_NORMAL; + PostNewEvent(animProps.event[packet_data[1]]); + }else{ + // TODO: Process other Bluetooth commands + } + + if(replyActive){ + rxValue[0] = '%'; + pTxCharacteristic->setValue(rxValue); + pTxCharacteristic->notify(); + } + // print params + ESP_LOGD(tag, "packet: %c%d, %d", rxValue[0], packet_data[0], packet_data[1]); + } + } +}; + +void extractCommand(char* packet, int* data, int* count) +{ + int base = 10; + //if(*packet == '#'){ + // base = 16; + //} + packet++; + + char* token = strtok(packet, ",\n"); + int8_t index = 0; + + while (token != NULL && index < MAX_PACKET_PARAMS) { + data[index] = strtol(token, NULL, base); + index++; + token = strtok(NULL, ",\n"); + } + *count = index; +} + +void BTSerial_Task(void *parameters){ + vTaskDelay(1000); + // Extend watchdog timer + esp_task_wdt_init(5, false); + + // Create the BLE Device. + BLEDevice::init(BLEDeviceName.c_str()); + + // Create the BLE Server + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + + // Create the BLE Service + BLEService *pService = pServer->createService((const char*)SERVICE_UUID.c_str()); + + // Create a BLE Characteristic + pTxCharacteristic = pService->createCharacteristic( (const char*)CHARACTERISTIC_UUID_TX.c_str(), NIMBLE_PROPERTY::NOTIFY ); + /*************************************************** + NOTE: DO NOT create a 2902 descriptor, it will be created auto.. if notifications + or indications are enabled on a characteristic. + + pCharacteristic->addDescriptor(new BLE2902()); + ****************************************************/ + pRxCharacteristic = pService->createCharacteristic( (const char*)CHARACTERISTIC_UUID_RX.c_str(), NIMBLE_PROPERTY::WRITE ); + pRxCharacteristic->setCallbacks(new MyCallbacks()); + // Start the service + pService->start(); + + // Start advertising + vTaskDelay(100); + pServer->getAdvertising()->start(); + ESP_LOGV(tag, "Waiting for a client..."); + ANIMATION_EVENT newEvent; + for(;;){ + //if (BTDeviceConnected) { + //pTxCharacteristic->setValue(&txValue, 1); + //pTxCharacteristic->notify(); + //txValue++; + //vTaskDelay(100); // bluetooth stack will go into congestion, if too many packets are sent + //} + + // disconnecting + if (!BTDeviceConnected && oldDeviceConnected) { + vTaskDelay(750); // give the bluetooth stack the chance to get things ready + pServer->startAdvertising(); // restart advertising + oldDeviceConnected = BTDeviceConnected; + newEvent.animIndex = POP_BLE_DISC; + newEvent.type = EV_INJECT; + PostNewEvent(newEvent); + ESP_LOGI(tag, "Client disconnected..."); + ESP_LOGI(tag, "Advertising again..."); + Buzzer_Play_Tune(TUNE_BLE_DISCONNECTED); + } + + // connecting + if (BTDeviceConnected && !oldDeviceConnected) { + // do stuff here on connecting + oldDeviceConnected = BTDeviceConnected; + newEvent.animIndex = POP_BLE_CONN; + newEvent.type = EV_INJECT; + PostNewEvent(newEvent); + ESP_LOGI(tag, "Client connected..."); + Buzzer_Play_Tune(TUNE_BLE_CONNECTED); + } + + vTaskDelay(200); + } + +} \ No newline at end of file diff --git a/temporary/Temp/BTSerial.h b/temporary/Temp/BTSerial.h new file mode 100644 index 0000000..1691151 --- /dev/null +++ b/temporary/Temp/BTSerial.h @@ -0,0 +1,16 @@ +#ifndef _BTSERIAL_H +#define _BTSERIAL_H + +#include + + +extern bool BTDeviceConnected; +extern String BLEDeviceName; + +void Init_BTSerial(void); + +void BTSerial_Task(void *parameters); + +void extractCommand(char* packet, int* data, int* count); + +#endif \ No newline at end of file diff --git a/temporary/Temp/MyNeoPixelBus.cpp b/temporary/Temp/MyNeoPixelBus.cpp new file mode 100644 index 0000000..74a81f6 --- /dev/null +++ b/temporary/Temp/MyNeoPixelBus.cpp @@ -0,0 +1,47 @@ +#include + +// Define constants for maximum LEDs +#define MAX_LEDS 300 // Adjust based on your LED requirements + +// Wrapper class for dynamic LED strips +class DynamicLedStrip { +private: + void* strip; // Pointer to the NeoPixelBus object + int numLeds; // Number of LEDs + String colorOrder; // Color order (for reference) + +public: + DynamicLedStrip() : strip(nullptr), numLeds(0), colorOrder("") {} + + // Initialize the LED strip + void initialize(int dataPin, int numLeds, const String& chipType, const String& colorOrder) { + this->numLeds = numLeds; + this->colorOrder = colorOrder; + + if (chipType == "WS2812" || chipType == "SK6812") { + if (colorOrder == "GRB") { + strip = new NeoPixelBus(numLeds, dataPin); + } else if (colorOrder == "RGB") { + strip = new NeoPixelBus(numLeds, dataPin); + } else if (colorOrder == "BGR") { + strip = new NeoPixelBus(numLeds, dataPin); + } + } + + if (strip) { + static_cast*>(strip)->Begin(); + } else { + Serial.println("Unsupported chipset or color order!"); + } + } + + // Set all LEDs to a specific color + void setColor(const RgbColor& color) { + if (strip) { + for (int i = 0; i < numLeds; i++) { + static_cast*>(strip)->SetPixelColor(i, color); + } + static_cast*>(strip)->Show(); + } + } +}; \ No newline at end of file diff --git a/temporary/Temp/command_processor.cpp b/temporary/Temp/command_processor.cpp new file mode 100644 index 0000000..8f4747c --- /dev/null +++ b/temporary/Temp/command_processor.cpp @@ -0,0 +1,71 @@ +#include "command_processor.h" + +int boothState[16]; + +void process_command(int* data, int paramCount) +{ + bool reply = false; + + switch(*data){ + case COMM_NULL: + break; + case COMM_REBOOT: + break; + case COMM_DIN: + break; + case COMM_DOUT: + break; + case COMM_RELAY: + break; + case COMM_AOUT: + break; + case COMM_AIN: + break; + case COMM_ECHO: + break; + case COMM_LED_STATUS: + break; + case COMM_GET_TEMP: + break; + case COMM_PLAY: + break; + case COMM_RF: + break; + case COMM_NEST: + break; + case COMM_BOOTH_STATE: + break; + case COMM_STRIP1: + + break; + case COMM_STRIP2: + break; + case COMM_OLED: + break; + default:; + } + + + if(reply){ + + } +} + +/* +int AppStates[16]; + +int getEvent(int index, int& ev){ + return ev[index]; + +} +enum BoothStates = { } +void RunAnimation(int stateIndex){ + switch (AppStates[stateIndex]){ + case 0: + break; + case 1: + break; + + } +} +*/ diff --git a/temporary/Temp/command_processor.h b/temporary/Temp/command_processor.h new file mode 100644 index 0000000..2367231 --- /dev/null +++ b/temporary/Temp/command_processor.h @@ -0,0 +1,31 @@ +#ifndef _COMMAND_PROCESSOR_H +#define _COMMAND_PROCESSOR_H + +extern int boothState[16]; + +void process_command(int* data, int paramCount); + +enum COMM_FUNC{ + COMM_NULL = 0, + COMM_REBOOT = 1, + COMM_DIN = 2, + COMM_DOUT = 3, + COMM_RELAY = 4, + COMM_AOUT = 5, + COMM_AIN = 6, + COMM_ECHO = 7, + COMM_LED_STATUS = 8, + COMM_GET_TEMP = 9, + COMM_PLAY = 10, + COMM_RF = 11, + + COMM_NEST = 30, + COMM_BOOTH_STATE = 50, + COMM_STRIP1 = 75, + COMM_STRIP2 = 100, + COMM_OLED = 125 +}; + + + +#endif diff --git a/temporary/Temp/firmware_html.h b/temporary/Temp/firmware_html.h new file mode 100644 index 0000000..7a7440f --- /dev/null +++ b/temporary/Temp/firmware_html.h @@ -0,0 +1,215 @@ +#ifndef _FIRMWARE_HTML_H +#define _FIRMWARE_HTML_H + +#include + +const char firmware_html_page[] PROGMEM = R"rawliteral( + + + + Firmware Update + + + + + +

Firmware Update

+
+
+ Local Update +
+
+ +
+
+ +
+
+ +
+ + + +
+ +
+ +
+
+
+ + + + +)rawliteral"; + + +#endif \ No newline at end of file diff --git a/temporary/Temp/githubCert.h b/temporary/Temp/githubCert.h new file mode 100644 index 0000000..40a476e --- /dev/null +++ b/temporary/Temp/githubCert.h @@ -0,0 +1,30 @@ +#ifndef CERT_H + +#define CERT_H + +const char * github_rootCACertificate = \ + "-----BEGIN CERTIFICATE-----\n" +"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n" +"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" +"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" +"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n" +"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n" +"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n" +"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n" +"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n" +"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" +"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n" +"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n" +"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n" +"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n" +"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n" +"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n" +"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n" +"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n" +"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" +"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n" +"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n" +"+OkuE6N36B9K\n" +"-----END CERTIFICATE-----\n"; + +#endif diff --git a/temporary/Temp/led_animation.cpp b/temporary/Temp/led_animation.cpp new file mode 100644 index 0000000..6b191af --- /dev/null +++ b/temporary/Temp/led_animation.cpp @@ -0,0 +1,1023 @@ +#include "led_animation.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "common/HSVTable.h" +#include "common/neo_colors.h" +#include "common/color_tools.h" + +static const char* tag = "led_anim"; + + +int AnimTestModeCount = 0; + +ANIM_STATUS animStatus; +WHITE_FILL_STATUS whiteStatus; +HUE_PALLET_DISPENSER HuePalletDispenser;// (1, 1, 1); + +/********************************************************************/ + +// Animation Loop Template +EXIT_TYPE Animation_Loop(ANIM_STATUS &stateMon, int msFreq, TickType_t duration, std::function callback){ + stateMon.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + EXIT_TYPE ret = EXIT_NORMAL; + + TickType_t startTicks = xTaskGetTickCount(); + + for(;;){ + TickType_t xLastWakeTime = xTaskGetTickCount(); + if(callback() == EXIT_FINISHED){ return EXIT_FINISHED; } + + if(ulTaskNotifyTake( pdTRUE, msFreq - ( xTaskGetTickCount() - xLastWakeTime ))){ // delay + stateMon.busy = false; + return EXIT_FROM_NOTIFY; + } + + // May have a duration limit + if(duration){ + if((xLastWakeTime - startTicks) > duration){ + return EXIT_TIMEOUT; + } + } + } + + return ret; +} + +int Const_White_Fill(WHITE_FILL_STATUS &status, FRONT_LIGHT light , float delayFactor, int countDown, int msFreq) { //int msRampUpTime, int msRampDownTime, int msHoldTime, int msFreq, float minDuty, float maxDuty){ + status.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + + // Linear brightness rise + if(light.maxDuty > 100) { light.maxDuty = 100; } + if(light.maxDuty < 1){ light.maxDuty = 1; } + + if (light.minDuty >= light.maxDuty) { light.minDuty = light.maxDuty -1; } + if (light.minDuty < 0) { light.minDuty = 0; } + + // Pre Delay + if(delayFactor > 0.0){ + int waitDelay = int((float)countDown * delayFactor); + ESP_LOGD(tag, "Const Light Delay: %d, countDown: %d", waitDelay, countDown); + if(ulTaskNotifyTake( pdTRUE, waitDelay)){ // delay + status.busy = false; + return EXIT_FROM_NOTIFY; + } + } + + int rampSteps = (countDown - ((float)countDown * delayFactor)) / msFreq; + float dutyStep = ( light.maxDuty - pwmOut[animProps.frontLight.relayIndex]->currDuty ) / rampSteps; + if(dutyStep < 0.1) { dutyStep = 0.1; } + + //Log.noticeln(" upTime: %d, hold: %d, downTime: %d, msFreq: %d, minDuty: %F, maxDuty: %F", msRampUpTime, msHoldTime, msRampDownTime, msFreq, minDuty, maxDuty); + //Log.noticeln(" dutyStep: %F", dutyStep); + // Ramp Up + for(;;){ + //calc new newDuty value + float newDuty = pwmOut[animProps.frontLight.relayIndex]->currDuty + dutyStep; + + //check if dslrbooth jumped ahead and catch up + if(delayFactor > 0){ + if(status.dslrCountStatus > newDuty && ((status.dslrCountStatus-10) < 90)){ + newDuty = status.dslrCountStatus; + } + } + + // Set the new Duty + if(newDuty > light.maxDuty){ newDuty = light.maxDuty; } + pwmOut[animProps.frontLight.relayIndex]->setOutput(newDuty); // set duty + + if(newDuty >= light.maxDuty) { break;} // break if complete + + //check if there was a trigger to leave function + if(ulTaskNotifyTake( pdTRUE, msFreq )){ goto done; } // delay + } + + // Hold Steady for msHoldTime + //Serial.printf("Holding at %.1f\n\r", pwmOut[animProps.frontLight.relay]->currDuty ); + pwmOut[animProps.frontLight.relayIndex]->setOutput(light.maxDuty); + if(ulTaskNotifyTake( pdTRUE, light.holdTime )){ goto done; } // delay + + // Linear Ramp Down to Min + rampSteps = light.rampTime / msFreq; + dutyStep = abs(( pwmOut[animProps.frontLight.relayIndex]->currDuty - light.minDuty) / rampSteps ); + if(dutyStep < .1) { dutyStep = .1; } + //Serial.printf("stepDown: %.1f\n\r", dutyStep); + + for(;;){ + pwmOut[animProps.frontLight.relayIndex]->setOutput( pwmOut[animProps.frontLight.relayIndex]->currDuty - dutyStep ); // set duty + if( pwmOut[animProps.frontLight.relayIndex]->currDuty <= light.minDuty ) { break; } // break if complete + + //if(status.repeat){ goto done;} + if(ulTaskNotifyTake( pdTRUE, msFreq )){ break; } // delay + } + + done: + pwmOut[animProps.frontLight.relayIndex]->setOutput(light.minDuty); + status.busy = false; + status.dslrCountStatus = 0; + return 0; +} + + +// TODO Roamer/Helio type White Fill +//int Const_White_Fill_Disc +/****************************************************************/ +int Animation_White_Fill(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msFillTime, rgbpixel_t& col, rgbpixel_t& baseCol) +{ + stateMon.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + + int msDelay = msFillTime / strip.effSize; + + // Fill background first + for(int i = 0; i < strip.effSize; i++){ + strip.pixels[i] = baseCol; // skips index calc + } + + // Linear fill + for(int i = 0; i < strip.effSize; i++){ + TickType_t xLastWakeTime = xTaskGetTickCount(); + strip.setPixel(i, col, 255); + strip.show(true); + if(ulTaskNotifyTake( pdTRUE, msDelay - (xTaskGetTickCount() - xLastWakeTime ) == pdTRUE )){ return 1; } // delay + } + + stateMon.busy = false; + return 0; +} + +/******************************** WHITE FILL MIRRORED *********************************/ +EXIT_TYPE Animation_White_Fill_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event) +{ + int msFillTime = 0; + if(event.countDown > 0){ // use countdown if available + msFillTime = event.countDown; + }else{ + msFillTime = map(event.param1, 0, 100, 0, 10000); + if(msFillTime < 1000){ msFillTime = 1000; } + } + + strip.powerDiv = (event.check4) ? 1 : 0; + + // TODO set msFreq from density + int msFreq = 25; + int halfSize = (strip.effSize / 2); + float steps = ((float)msFreq * (float)halfSize) / msFillTime; + strip.fill(col_black, 0, strip.effSize); // clear/off all pixels + + float stepsTotal = steps; + //int startIndex = 0; + int endIndex = stepsTotal; + //uint8_t frac = 0; + ESP_LOGD(tag, "whitefill-> msFillTime: %d, halfsize: %d, steps: %.3f, freq: %d, hue: %d", msFillTime, halfSize, steps, msFreq, event.hue); + + rgbpixel_t mainCol = GetColorFromHue(event.hue); + //rgbpixel_t fracCol; // = event.col1; + + int lastEndIndex = 0; + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, [&](){ + + stepsTotal += steps; + endIndex = (int)stepsTotal; + if(endIndex != lastEndIndex){ + for(int i = lastEndIndex; i < endIndex; i++){ + strip.setPixelMirrored(i, mainCol); + } + lastEndIndex = endIndex; + strip.show(true); + } + + if(endIndex > (halfSize -1)){ return EXIT_FINISHED; } + + /* + frac = (stepsTotal - (int)stepsTotal) * 255; + fracCol = mainCol; + scalePixel(fracCol, frac); + linearizePixel(fracCol); + strip.setPixelMirrored(endIndex + 1, fracCol); + */ + //strip.show(true); + + return EXIT_NORMAL; + }); + + // TODO remove this trace later + //Log.traceln("exited white fill"); + return ret; +} + +float calculateSteps(int msFreq, float LEDCount, int msFillTime) { + return (msFreq * LEDCount) / msFillTime; +} + +void GetOptimizedStepInterval(float &steps, int &interval, int LEDCount, int msFillTime, int startInterval, float tolerance) +{ + steps = calculateSteps(startInterval, LEDCount, msFillTime); + + while (std::abs(steps - std::round(steps)) > tolerance) { + steps = calculateSteps(++startInterval, LEDCount, msFillTime); + } + interval = startInterval; + steps = round(steps); +} + + +EXIT_TYPE Animation_Linear_Brighten(LEDSTRIP& strip, ANIMATION_EVENT& event){ + return EXIT_NORMAL; +} + + +EXIT_TYPE Animation_Tick_Fill(LEDSTRIP& strip, ANIMATION_EVENT& event){ + return EXIT_NORMAL; +} + + + +/******************************** COLORED SNAKES *********************************/ +EXIT_TYPE Animation_Snakes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType, int cycles) +{ + bool mix = event.check1; + bool rotate = event.check2; + int sectors = constrain(event.param1/10, 1, 10); + int colorCount = constrain(event.param2/10, 1, 10); + strip.powerDiv = (event.check4) ? 1 : 0; + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + int snakeSize = trunc(roundf(strip.effSize / (float)sectors)); + + HuePalletDispenser.Initialize(event.hue, event.hueRange, colorCount); + ESP_LOGD(tag, " size: %d, sectors: %d, freq: %d", snakeSize, sectors, msFreq); + + // Create Color Pallet Array + rgbpixel_t col[colorCount]; + if(colorCount == 1){ + col[0] = GetColorFromHue(event.hue); + }else{ + for(int i = 0; i < colorCount; i++){ + float hue = HuePalletDispenser.GetNextPalletHue(); + col[i] = HUEtoRGB( hue ); + //ESP_LOGD(tag, " hue%d: ", hue); + } + } + + + LED_DIR dir = (dirType == DT_FWD)? DIR_FWD : DIR_REV; + int dirVal = (dir==DIR_FWD)? 1 : -1; + int pixIndex = 0, rotateOffset = 0, colIndex = 0; + bool colorCycle = true; + + strip.fill({0,0,0}, 0, strip.effSize); + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + + if(colorCycle){ + for(int sector = 0; sector < sectors; sector++){ + int pix = (sector * snakeSize) + pixIndex + rotateOffset; + if(mix){ + strip.setPixel(pix * dirVal, (rgbpixel_t)col[(colIndex + sector)%colorCount]); + }else{ + strip.setPixel(pix * dirVal, (rgbpixel_t)col[colIndex]); + } + } + }else{ + for(int sector = 0; sector < sectors; sector++){ + int pix = (sector * snakeSize) + pixIndex + rotateOffset; + strip.setPixel(pix * dirVal, {0,0,0}); + } + } + + if(rotate){ // turn off front pixels + strip.rotatePixels(dir); + rotateOffset = ++rotateOffset % strip.effSize; + } + strip.show(true); + + pixIndex = ++pixIndex % snakeSize; + + if(pixIndex == 0){ + if(colorCycle){ + colorCycle = false; + }else{ + // Check if there's an iteration limit + if(cycles){ + if(--cycles <= 0){ return EXIT_FINISHED; } + } + // Once full cycle here + if(dirType == DT_BOTH){ + dir = (dir == DIR_FWD)? DIR_REV : DIR_FWD; + dirVal = -dirVal; + } + colorCycle = true; + colIndex = ++colIndex % colorCount; // next color + } + } + return EXIT_NORMAL; + })); + + return ret; +} + + +/******************************** COLORED COMETS *********************************/ +#define COMET_SIZE_FACTOR 0.15 +#define COMET_FADE_FACTOR1 128 +#define COMET_FADE_FACTOR2 192 + +EXIT_TYPE Animation_Comets(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType, int cycles){ + int sectorCount = constrain(event.param1/10, 1, 10); // cometCount also + int colorCount = constrain(event.param2/10, 1, 10); // color divisions + bool mix = event.check1; + bool rotate = true; + bool randomDecay = event.check2; + bool shorterTail = event.check3; + uint8_t fadeFactor = COMET_FADE_FACTOR1; + if(shorterTail){ fadeFactor = COMET_FADE_FACTOR2; } + strip.powerDiv = (event.check4) ? 1 : 0; + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + int sectorSize = trunc(roundf(strip.effSize / (float)sectorCount)); + + int cometSize = (strip.effSize / sectorCount) * COMET_SIZE_FACTOR; + if(cometSize < 1){ cometSize = 1;} + + HuePalletDispenser.Initialize(event.hue, event.hueRange, colorCount); + ESP_LOGD(tag, " size: %d, sectorCount: %d, freq: %d", sectorSize, sectorCount, msFreq); + + // Create Color Pallet Array + rgbpixel_t col[colorCount]; + if(colorCount == 1){ + col[0] = GetColorFromHue(event.hue); + }else{ + for(int i = 0; i < colorCount; i++){ + float hue = HuePalletDispenser.GetNextPalletHue(); + col[i] = HUEtoRGB( hue ); + //ESP_LOGD(tag, " hue%d: ", hue); + } + } + + LED_DIR dir; + if(dirType == DT_BOTH){ + dir = DIR_FWD; + }else{ + dir = (dirType == DT_FWD)? DIR_FWD : DIR_REV; + } + + strip.fill({0,0,0}, 0, strip.effSize); + + int rotateIndex = 0; + int cycleCount = 0; + int colIndex = 0; + int rotOffset = 0; + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + + // Random Tail Decay here.... + if(randomDecay){ + for (int j = 0; j < strip.effSize; j++){ + if (random(10) > 5){ + fadeToBlackBy(strip.pixels[j], fadeFactor); + } + } + }else{ // Regular Tail Decay + for (int j = 0; j < strip.effSize; j++){ + fadeToBlackBy(strip.pixels[j], fadeFactor); + } + } + + // Draw Comets + if(mix){ + for(int sector = 0; sector < sectorCount; sector++){ + strip.fill((rgbpixel_t)col[(colIndex + sector) % colorCount], rotOffset + (sector * sectorSize), cometSize); + } + }else{ + for(int sector = 0; sector < sectorCount; sector++){ + strip.fill((rgbpixel_t)col[colIndex], rotOffset + (sector * sectorSize), cometSize); + } + } + + rotateIndex = ++rotateIndex % strip.effSize; + + if(dir == DIR_FWD){ rotOffset++; } + else{ rotOffset--; } + + // full rotation + if(rotateIndex == 0){ + // reverse rotation + if(dirType == DT_BOTH){ + dir = (dir == DIR_FWD)? DIR_REV : DIR_FWD; + } + + if(cycles){ + cycleCount++; + if(cycleCount >= cycles){ + return EXIT_FINISHED; + } + } + + colIndex = ++colIndex % colorCount; // Increment color index + } + strip.show(true); + + return EXIT_NORMAL; + })); + + return ret; +} + +void fadeToBlackBy(rgbpixel_t& pixel, uint8_t fadeAmount) { + pixel.red = (pixel.red <= 8) ? 0 : pixel.red - ((pixel.red * fadeAmount) >> 8); + pixel.grn = (pixel.grn <= 8) ? 0 : pixel.grn - ((pixel.grn * fadeAmount) >> 8); + pixel.blu = (pixel.blu <= 8) ? 0 : pixel.blu - ((pixel.blu * fadeAmount) >> 8); +} + +inline uint8_t nscale8(uint8_t i, uint8_t scale) { + return (((int)i * (int)(scale)) >> 8) + ((i && scale) ? 1 : 0); +} + + +/******************************** COLORED SECTORS *********************************/ +EXIT_TYPE Animation_Sectors(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType, int cycles){ + bool rotate = event.check2; + int sectorCount = constrain(event.param1/10, 2, 10); + int colorCount = constrain(event.param2/10, 1, 10); + + strip.powerDiv = (event.check4) ? 1 : 0; + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + int sectorSize = trunc(roundf(strip.effSize / (float)sectorCount)); + + ESP_LOGD(tag, "hue: %d, range: %d\n", event.hue, event.hueRange); + HuePalletDispenser.Initialize(event.hue, event.hueRange, colorCount); + ESP_LOGD(tag, "size: %d, sectorCount: %d, colors: %d, freq: %d", sectorSize, sectorCount, colorCount, msFreq); + + // Create Color Pallet Array + rgbpixel_t col[colorCount]; + if(colorCount == 1){ + col[0] = GetColorFromHue(event.hue); + }else{ + for(int i = 0; i < colorCount; i++){ + float hue = HuePalletDispenser.GetNextPalletHue(); + col[i] = HUEtoRGB( hue ); + //ESP_LOGD(tag, "hue%d: ", hue); + } + } + + LED_DIR dir; + if(dirType == DT_BOTH){ + dir = DIR_FWD; + }else{ + dir = (dirType == DT_FWD)? DIR_FWD : DIR_REV; + } + + strip.fill({0,0,0}, 0, strip.effSize); + + bool updateColors = true; + int rotateIndex = 0; + int cycleCount = 0; + int colIndex = 0; + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + + if(updateColors){ // + for(int sector = 0; sector < sectorCount; sector++){ + strip.fill((rgbpixel_t)col[(colIndex + sector) % colorCount], sector * sectorSize, sectorSize); + } + colIndex = ++colIndex % colorCount; // Increment color index + updateColors = false; + } + + if(rotate){ // turn off front pixels + strip.rotatePixels(dir); + rotateIndex = ++rotateIndex % strip.effSize; + + // full rotation + if(rotateIndex == 0){ + // reverse rotation + if(dirType == DT_BOTH){ + dir = (dir == DIR_FWD)? DIR_REV : DIR_FWD; + } + + if(cycles){ + cycleCount++; + if(cycleCount >= cycles){ + return EXIT_FINISHED; + } + } + + updateColors = true; + } + } + + strip.show(true); + return EXIT_NORMAL; + })); + + return ret; +} + + +/******************************** DASHES SECTORS *********************************/ +EXIT_TYPE Animation_Dashes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType, int cycles){ + int sectorCount = constrain(event.param1/10, 1, 10); + int colorCount = constrain(event.param2/10, 1, 10); + bool mix = event.check1; + bool rotate = event.check2; + + strip.powerDiv = (event.check4) ? 1 : 0; + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + int sectorSize = trunc(roundf(strip.effSize / (float)sectorCount)); + + HuePalletDispenser.Initialize(event.hue, event.hueRange, colorCount); + ESP_LOGD(tag, " size: %d, sectorCount: %d, freq: %d", sectorSize, sectorCount, msFreq); + + // Create Color Pallet Array + rgbpixel_t col[colorCount]; + if(colorCount == 1){ + col[0] = GetColorFromHue(event.hue); + }else{ + for(int i = 0; i < colorCount; i++){ + float hue = HuePalletDispenser.GetNextPalletHue(); + col[i] = HUEtoRGB( hue ); + //ESP_LOGD(tag, " hue%d: ", hue); + } + } + + LED_DIR dir; + if(dirType == DT_BOTH){ + dir = DIR_FWD; + }else{ + dir = (dirType == DT_FWD)? DIR_FWD : DIR_REV; + } + + strip.fill({0,0,0}, 0, strip.effSize); + + bool updateColors = true; + int rotateIndex = 0; + int cycleCount = 0; + int colIndex = 0; + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + if(updateColors){ // + if(mix){ + for(int sector = 0; sector < sectorCount; sector++){ + strip.fill((rgbpixel_t)col[(colIndex + sector) % colorCount], sector * sectorSize, sectorSize/2); + } + }else{ + for(int sector = 0; sector < sectorCount; sector++){ + strip.fill((rgbpixel_t)col[colIndex], sector * sectorSize, sectorSize/2); + } + } + + colIndex = ++colIndex % colorCount; // Increment color index + updateColors = false; + } + + if(rotate){ // turn off front pixels + strip.rotatePixels(dir); + rotateIndex = ++rotateIndex % strip.effSize; + + // full rotation + if(rotateIndex == 0){ + // reverse rotation + if(dirType == DT_BOTH){ + dir = (dir == DIR_FWD)? DIR_REV : DIR_FWD; + } + + if(cycles){ + cycleCount++; + if(cycleCount >= cycles){ + return EXIT_FINISHED; + } + } + + updateColors = true; + } + } + + strip.show(true); + + return EXIT_NORMAL; + })); + + return ret; +} + + +/****************************************************************/ +void Animation_TransTo_Color(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msTransTime, int msFreq, rgbpixel_t newCol) +{ + stateMon.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + + int numSteps = (msTransTime / msFreq); + if (numSteps == 0) { return; } + + for (uint8_t step = 0; step < numSteps; step++) { + TickType_t xLastWakeTime = xTaskGetTickCount(); + + for (size_t i = 0; i < strip.effSize; i++) { + rgbpixel_t colorDiff = { + .red = static_cast((newCol.red - strip.pixels[i].red) / numSteps), + .grn = static_cast((newCol.grn - strip.pixels[i].grn) / numSteps), + .blu = static_cast((newCol.blu - strip.pixels[i].blu) / numSteps) + }; + strip.pixels[i].red += colorDiff.red; + strip.pixels[i].grn += colorDiff.grn; + strip.pixels[i].blu += colorDiff.blu; + } + + strip.show(true); + if(ulTaskNotifyTake( pdTRUE, msFreq - (xTaskGetTickCount() - xLastWakeTime ) == pdTRUE )){ return; } // delay + } + stateMon.busy = false; +} + + +/******************************** HUE SPECTRUM MIRRORED *********************************/ + +EXIT_TYPE Animation_Hue_Spectrum_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration){ + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + strip.powerDiv = (event.check4) ? 1 : 0; + Fill_Hue_Spectrum_Mirrored(strip, event.hue, event.hueRange); + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, duration, std::function([&]() -> int { + strip.show(true); + strip.rotatePixels(DIR_FWD); + return EXIT_NORMAL; + })); + + return ret; +} + +void Fill_Hue_Spectrum(LEDSTRIP& strip, float hue, float range) +{ + HuePalletDispenser.Initialize(hue, range, strip.effSize); + for(int i = 0; i < strip.effSize; i++){ + strip.setPixel(i, HUEtoRGB( HuePalletDispenser.GetNextPalletHue() )); + } +} + +void Fill_Hue_Spectrum_Mirrored(LEDSTRIP& strip, float hue1, float hue2) +{ + HuePalletDispenser.Initialize(hue1, hue2, strip.effSize / 2.0); + + for(int i = 0; i < strip.effSize/2; i++){ + strip.setPixelMirrored(i, HUEtoRGB( HuePalletDispenser.GetNextPalletHue() )); + } +} + + +/******************************** PULSE COLOR CYCLING *********************************/ +//TODO Add Linearizing for smoother transitions +#define PWR_STEP 8 +EXIT_TYPE Animation_Pulse_Color_Cycling(LEDSTRIP& strip, ANIMATION_EVENT& event){ + int colorSteps = event.param1/10.0; + if(colorSteps < 3){ colorSteps = 2; } + + strip.powerDiv = (event.check4) ? 1 : 0; + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + + HuePalletDispenser.Initialize(event.hue, event.hueRange, colorSteps); + + int pwrStep = PWR_STEP; + int pwr = pwrStep; + + rgbpixel_t col = HUEtoRGB(HuePalletDispenser.GetNextPalletHue()); + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + // ramp up/down brightness + rgbpixel_t col_pwr = col; + scalePixel(col_pwr, (uint8_t)pwr); + linearizePixel(col_pwr); + strip.fill(col_pwr, 0, strip.effSize); + strip.show(true); + + pwr += pwrStep; + if(pwr >= (256-PWR_STEP) || pwr <= PWR_STEP){ + pwrStep = -pwrStep; // switch brightness direction + + // Change color + if(pwrStep > 0){ + col = HUEtoRGB(HuePalletDispenser.GetNextPalletHue()); + } + } + + return EXIT_NORMAL; + })); + + return ret; +} + + +/******************************** RAINBOW *********************************/ +// TODO Need more Red for rainboow +EXIT_TYPE Animation_Rainbow(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration){ + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + double hueStep = 359.2 / (strip.effSize-1); + + strip.powerDiv = (event.check4) ? 1 : 0; + + for(int i = 0; i < strip.effSize; i++){ + strip.setPixel(i, HUEtoRGB((int)round(i*hueStep))); + } + + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, duration, std::function([&]() -> int { + strip.rotatePixels(DIR_FWD); + strip.show(true); + return EXIT_NORMAL; + })); + + return ret; +} + + +/******************************** FIRE ANIMATION *********************************/ +//TODO Get these fire params from json +#define FIRE_SPARK_PIXEL_PERCENT 15 +#define MIN_FIRE_COOLING 20 +#define MAX_FIRE_COOLING 100 +#define MIN_FIRE_SPARKING 50 +#define MAX_FIRE_SPARKING 200 +uint8_t *heat; +uint8_t sparkCount; + +EXIT_TYPE Animation_Fire(LEDSTRIP& strip, ANIMATION_EVENT& event){ + int msFreq = CalcEventInterval(event.speed, MIN_LED_UPDATE_INTERVAL, 100); + bool cycleColors = event.check1; + uint8_t sparking = map(event.param1, 0, 100, MIN_FIRE_SPARKING, MAX_FIRE_SPARKING); // input, input max, output min, out max + uint8_t cooling = map(event.param2, 0, 100, MIN_FIRE_COOLING, MAX_FIRE_COOLING); + + strip.powerDiv = (event.check4) ? 1 : 0; + + int duration = map(event.hueRange, 0, 360, 0, 60000 ); // upto 1 minute + if(cycleColors && duration == 0) { duration = 10000; } + if(!cycleColors && duration > 0) { duration = 0; } + + FIRE_COLOR fireColor = RED_FIRE; + + // Choose which Color comes closest + if(event.hue = -1 || event.hue == -2) { event.hue == 0;} + rgbpixel_t col = HUEtoRGB(event.hue); + if (col.red >= col.grn && col.red >= col.blu) { + fireColor = RED_FIRE; + } else if (col.grn >= col.red && col.grn >= col.blu) { + fireColor = GREEN_FIRE; + } else { + fireColor = BLUE_FIRE; + } + + EXIT_TYPE ret; + while(1){ + ret = Animation_Loop( animStatus, msFreq, duration, std::function([&]() -> int { + Fire_Update( strip, fireColor, cooling, sparking); + strip.show(true); + return EXIT_NORMAL; + })); + + if(ret == EXIT_TIMEOUT){ + fireColor = (FIRE_COLOR)(((int)fireColor + 1) % 3); + continue; + } + break; + } + + return ret; +} + +void LEDStrip_FireInit(LEDSTRIP& strip){ + sparkCount = (strip.effSize/2 * FIRE_SPARK_PIXEL_PERCENT) / 100; + heat = new uint8_t[strip.effSize/2]; + for(int i = 0; i < (strip.effSize/2); i++){ + heat[i] = 0; + } + + srand(70); +} + +void Fire_Update( LEDSTRIP& strip, FIRE_COLOR fire, int Cooling, int Sparking) { + int cooldown; + + // Step 1. Cool down every cell a little + for( int i = 0; i < (strip.effSize/2); i++) { + cooldown = rand() % ((Cooling * 10) / (strip.effSize/2) + 2); + + if(cooldown > heat[i]) { + heat[i] = 0; + }else { + heat[i] -= cooldown; + } + } + + // Step 2. Heat from each cell drifts 'up' and diffuses a little + // Starts from farthest and work down to the source + for( int k = (strip.effSize/2) - 1; k >= 2; k--) { + heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3; + } + + // Step 3. Randomly ignite new 'sparks' near the bottom + if( (rand() % 255) < Sparking ) { + int y = (rand() % sparkCount); + heat[y] += 160 + (rand() % 95); + } + + // Step 4. Convert heat to LED colors + for( int j = 0; j < strip.effSize/2; j++) { + strip.setPixelMirrored(j, GetPixelHeatColor(fire, heat[j])); + } +} + +rgbpixel_t GetPixelHeatColor ( FIRE_COLOR fire, uint8_t temperature){ + rgbpixel_t led; + + // Scale 'heat' down from 0-255 to 0-191 + //uint8_t T = ((uint16_t)(temperature*191))>>8; + uint8_t T = (uint16_t)(temperature*120) >> 8; + + // calculate ramp up from (heat ramp) + int hr = T & 0x3F; // 0..63 + hr <<= 2; // scale up to 0..252 + + // Choose Fire Color + switch(fire){ + case RED_FIRE:{ + //if((hr + 32) & 255){ hr = 255; } + if((hr + 32) > 255){ hr = 255; } + // figure out which third of the spectrum we're in: + if( T > 117) { // hottest + led = (rgbpixel_t){ .red=120, .grn=120, .blu=(uint8_t)hr }; // spark color + } else if( T > 48 ) { // middle + led = (rgbpixel_t){ .red=255, .grn=(uint8_t)hr, .blu=0 }; // flame color + } else{ // coolest + led = (rgbpixel_t){ .red=(uint8_t)hr, .grn=0, .blu=0 }; // cooling color + } + break; + } + case BLUE_FIRE:{ + if( T > 117) { // hottest + led = (rgbpixel_t){ .red=120, .grn=(uint8_t)hr, .blu=120 }; + } else if( T > 48 ) { // middle + led = (rgbpixel_t){ .red=(uint8_t)hr, .grn=0, .blu=255 }; + } else { // coolest + led = (rgbpixel_t){ .red=0, .grn=0,.blu=(uint8_t)hr }; + } + break; + } + default:{ // GRN_FIRE + if( T > 117) { // hottest + led = (rgbpixel_t){ .red=(uint8_t)hr, .grn=120, .blu=120 }; + }else if ( T > 48 ) { // middle + led = (rgbpixel_t){ .red=0, .grn=255, .blu=(uint8_t)hr }; + }else { // coolest + led = (rgbpixel_t){ .red=0, .grn=(uint8_t)hr, .blu=0 }; + } + break; + } + } + + return led; +} + + + +/******************************** Serial Out Toggle *********************************/ +EXIT_TYPE Animation_Serial_Test2(LEDSTRIP& strip, int msFreq, const char* s1, const char* s2){ + //Animation_Loop( animStatus, msFreq, [&](){ + EXIT_TYPE ret = Animation_Loop( animStatus, msFreq, INFINITE_LOOP, std::function([&]() -> int { + static bool x = false; + + if(x){ + Serial.print("[ "); Serial.print( s1 ); Serial.print(" ]\r"); + }else{ + Serial.print("[ "); Serial.print( s2 ); Serial.print(" ]\r"); + } + x = !x; + + strip.show(true); + return EXIT_NORMAL; + })); + + return ret; +} + + +/******************************** *********************************/ + +void Animation_SparkleHue(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msFreq, uint8_t hueRange, uint8_t hue, uint8_t sat, uint8_t val) +{ + stateMon.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + + for(;;){ + TickType_t xLastWakeTime = xTaskGetTickCount(); + + //decay all leds + for(int i = 0; i < strip.effSize; i++){ + // strip.scale(strip.pixels[i], 0.5 * 0xFF); + } + + // Random Pixel + //int index = random(strip.effSize); + //uint16_t h = (random(hueRange * 2) - hueRange) % HUE_MAX; + //strip.setPixel(index, h, sat, val); + + strip.show(true); + if(ulTaskNotifyTake( pdTRUE, msFreq - (xTaskGetTickCount() - xLastWakeTime ) == pdTRUE )){ return; } // delay + } + + stateMon.busy = false; +} + +void Animation_SparkleColor(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msFreq, rgbpixel_t col) +{ + stateMon.busy = true; + ulTaskNotifyTake( pdTRUE, 0); + + for(;;){ + TickType_t xLastWakeTime = xTaskGetTickCount(); + + //decay all leds + for(int i = 0; i < strip.effSize; i++){ + // strip.scale(strip.pixels[i], 0.5 * 0xFF); + } + + // Random Pixel + int index = random(strip.effSize); + strip.pixels[index] = col; + + strip.show(true); + if(ulTaskNotifyTake( pdTRUE, msFreq - (xTaskGetTickCount() - xLastWakeTime ) == pdTRUE )){ return; } // delay + } + + stateMon.busy = false; +} + + + +/******************************** test Functions *********************************/ +EXIT_TYPE Animation_SetPixel(LEDSTRIP& strip, ANIMATION_EVENT& event){ + return EXIT_NORMAL; +} + +EXIT_TYPE Animation_Clear(LEDSTRIP& strip, ANIMATION_EVENT& event){ + return EXIT_NORMAL; +} + + + + + + + + +/******************************** *********************************/ + +// This takes into account that the web color picker hue is reversed +float GetHueRangeWeb(float hue1, float hue2){ + float rng; + if (hue1 > hue2) { rng = hue1 - hue2; } + else { rng = 360.0 + hue1 - hue2; } + + if (rng < 0.0) { rng += 360.0; } + else if (rng > 360.0) { rng -= 360.0; } + + return rng; +} + +float GetHueRange(float hue1, float hue2){ + float range = hue2 - hue1; + if(hue2 < hue1){ range += 360.0; } + //Log.noticeln(" hue range: %d", hueRange); + return range; +} + + + +/******************************** SPEED CALC HELPER FUNCTIONS *********************************/ + +void Init_Speed_Range(float startSpeed, float endSpeed, int steps){ + +} + +float GetNextSpeed(void){ + return 0.0; +} + + + +int CalcEventInterval(float speed, int min, int max){ + int range = max - min; + return (max - ((speed/100.0) * range)); +} + +rgbpixel_t GetColorFromHue(int hue){ + if(hue == -1){ + return col_white; + }else if(hue == -2){ + return col_black; + }else if(hue >= 0 && hue <= 360){ + return HUEtoRGB(hue); + } + return col_black; +} + + + \ No newline at end of file diff --git a/temporary/Temp/led_animation.h b/temporary/Temp/led_animation.h new file mode 100644 index 0000000..21ed576 --- /dev/null +++ b/temporary/Temp/led_animation.h @@ -0,0 +1,205 @@ +#ifndef _LED_ANIMATION_H +#define _LED_ANIMATION_H + +#include "common/LEDStrip.h" +#include "led_strip.h" +#include "my_board.h" +#include +#include "common/HSVTable.h" + +#define HUE_MAX 767 + +enum EXIT_TYPE { EXIT_NORMAL, EXIT_FROM_NOTIFY, EXIT_FINISHED, EXIT_TIMEOUT }; + + +#define HUE_CALC_METHOD 0 +// TODO Fix hue range... 0-359 or 0-360; +#if HUE_CALC_METHOD == 0 + #define HUEtoRGB(x) trueHSV(x) +#elif HUE_CALC_METHOD == 1 + #define HUEtoRGB(x) sineHSV(x) +#elif HUE_CALC_METHOD == 2 + #define HUEtoRGB(x) pwrHSV(x) +#endif + +#define RGBtoHUE(x) RGBToHSV(x) + +#define INFINITE_LOOP 0 + + +enum DIR_TYPE { DT_REV, DT_FWD, DT_BOTH }; + +typedef struct { + int RampUpTime; + int msRampDownTime; + int msHoldTime; + int msFreq; + float minDuty; + float maxDuty; + int interval; + uint8_t pwr; +}params_whiteFill; + +typedef struct { + int hue; + int cooling; + int sparking; + int interval; + uint8_t pwr; +}params_fire_hue; + +typedef struct { + rgbpixel_t col1; + rgbpixel_t col2; + uint8_t range; /* hue range 0-127*/ + int density; + uint8_t step; /* range step inteval*/ + int interval; + uint8_t pwr; +}params_rain; + +#define ANIMTESTMODE_TIMEOUT (20 * 1000) // 20 secs + +typedef struct { + int EventTestCountdown = 0; + int EventsIndex; // upto 8 events based on anim-events.json + int EventsCount; // upto 8 events based on anim-events.json + int PopupAnimIndex; + bool busy; + bool repeat; + int countStatus; +}ANIM_STATUS; + +typedef struct { + bool busy; + int dslrCountStatus; +}WHITE_FILL_STATUS; + +extern ANIM_STATUS animStatus; +extern WHITE_FILL_STATUS whiteStatus; + +float calculateSteps(int msFreq, float LEDCount, int msFillTime); +void GetOptimizedStepInterval(float &steps, int &interval, int LEDCount, int msFillTime, int startInterval, float tolerance); + +float GetNextPalletHue(void); +void Init_Hue_Range_Pallet(float hue1, float hue2, int colSteps); + +float GetHueRange(float hue1, float hue2); +float GetHueRangeWeb(float hue1, float hue2); + +int CalcEventInterval(float speed, int min, int max); + +/******************************** ANIMATION LOOP HELPER ********************************* + * All Animations should use this lopp helper. + * Logic for monitoring exit notifications + * Timeout in mSec can be set to exit +**************************************************************************************/ +EXIT_TYPE Animation_Loop(ANIM_STATUS &stateMon, int msFreq, TickType_t duration, std::function callback=NULL); + +// min:0-100, max:0-100 +//int Const_White_Fill(WHITE_FILL_STATUS &status, int msRampUpTime, int msHoldTime, int msRampDownTime, int msFreq, float minDuty, float maxDuty); +int Const_White_Fill(WHITE_FILL_STATUS &status, FRONT_LIGHT light , float delayFactor, int countDown, int msFreq); +//int Const_White_Fill_HelioType(ANIM_STATUS &stateMon, float startPoint, int msRampUpTime, int msRampDownTime, int msHoldTime, int msFreq, float minDuty, float maxDuty); + +EXIT_TYPE Animation_White_Fill_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event); + +EXIT_TYPE Animation_Linear_Brighten(LEDSTRIP& strip, ANIMATION_EVENT& event); +EXIT_TYPE Animation_Tick_Fill(LEDSTRIP& strip, ANIMATION_EVENT& event); + + +/******************************** COLORED SNAKES ********************************* + * cycles=0=infinite + * (check1) "mix"=false, all appearing sectors will be the same color until the next iteration + * "mix"=true, each meteor color will be different + * (check2) "roate", add rotation to animation + * (hue range) hue range (0-360) + * (param1) sectors + * (param2) colorCount, number of color divisions +**************************************************************************************/ +EXIT_TYPE Animation_Snakes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0); +void drawSnakeMix(LEDSTRIP& strip, int sectors, float hue1, float hue2, int colorCount); + + +/******************************** COLORED COMETS ********************************* + *(check1) "mix"=false, all appearing comets will be the same color until the next iteration + * (check2) "mix"=true, each comets color will be different + * (param1) hue range (0-360) + * (param2) colorCount, number of color divisions +**************************************************************************************/ +EXIT_TYPE Animation_Comets(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0); + +void fadeToBlackBy(rgbpixel_t& pixel, uint8_t fadeAmount); +inline uint8_t nscale8(uint8_t i, uint8_t scale); + +/******************************** COLORED SECTORS ********************************* + * (check1) "mix"=false, all appearing sectors will be the same color until the next iteration + * (check2) "mix"=true, each meteor color will be different + * (param1) hue range (0-360) + * (param2) colorCount, number of color divisions +**************************************************************************************/ +EXIT_TYPE Animation_Sectors(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dir=DT_BOTH, int cycles=0); + + +/******************************** COLORED DASHES ********************************* + * (check1) "mix"=false, all appearing dashes will be the same color until the next iteration + * (check2) "mix"=true, each dash color will be different + * (param1) hue range (0-360) + * (param2) colorCount, number of color divisions +**************************************************************************************/ +EXIT_TYPE Animation_Dashes(LEDSTRIP& strip, ANIMATION_EVENT& event, DIR_TYPE dirType=DT_BOTH, int cycles=0); + + +/******************************** HUE SPECTRUM ********************************* + * (check1) "mix"=false, all appearing dashes will be the same color until the next iteration + * (check2) "mix"=true, each dash color will be different + * (param1) hue range (0-360) + * (param2) colorCount, number of color divisions +**************************************************************************************/ +EXIT_TYPE Animation_Hue_Spectrum(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration); +EXIT_TYPE Animation_Hue_Spectrum_Mirrored(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration); +void Fill_Hue_Spectrum(LEDSTRIP& strip, float hue1, float hue2); +void Fill_Hue_Spectrum_Mirrored(LEDSTRIP& strip, float hue1, float hue2); + + +/************************ RAINBOW *********************/ +EXIT_TYPE Animation_Rainbow(LEDSTRIP& strip, ANIMATION_EVENT& event, TickType_t duration); + +/************************ PULSE COLOR *********************/ +EXIT_TYPE Animation_Pulse_Color_Cycling(LEDSTRIP& strip, ANIMATION_EVENT& event); + + +/**************** FIRE ****************/ +enum FIRE_COLOR { RED_FIRE, GREEN_FIRE, BLUE_FIRE}; +//void Animation_Fire(LEDSTRIP& strip, ANIMATION_EVENT& event, FIRE_COLOR fire, TickType_t duration); +EXIT_TYPE Animation_Fire(LEDSTRIP& strip, ANIMATION_EVENT& event); +void LEDStrip_FireInit(LEDSTRIP& strip); +void Fire_Update( LEDSTRIP& strip, FIRE_COLOR fire, int Cooling, int Sparking); +rgbpixel_t GetPixelHeatColor ( FIRE_COLOR fire, uint8_t temperature); + + + + +/************************ *********************/ +void Animation_Splash(LEDSTRIP& strip); +void Animation_SparkleColor(LEDSTRIP& strip, int msFreq); +void Animation_SparkleHue(LEDSTRIP& strip, ANIMATION_EVENT& event); + +EXIT_TYPE Animation_Serial_Test2(LEDSTRIP& strip, int msFreq, const char* s1, const char* s2); +// msTransTime(Duration), msFreq(update interval) +void Animation_TransTo_Color(LEDSTRIP& strip, ANIM_STATUS &stateMon, int msTransTime, int msFreq, rgbpixel_t col); + + +void drawSectorsHueSingle(LEDSTRIP& strip, rgbpixel_t col, float hue1, float hue2, int sectors) ; +void drawSectorsHueMix(LEDSTRIP& strip, float hue1, float hue2, int sectors, int colorCount); + + + +/************************ TEST ANIMATIONS *********************/ +EXIT_TYPE Animation_SetPixel(LEDSTRIP& strip, ANIMATION_EVENT& event); +EXIT_TYPE Animation_Clear(LEDSTRIP& strip, ANIMATION_EVENT& event); + + +rgbpixel_t GetColorFromHue(int hue); + + +#endif \ No newline at end of file diff --git a/temporary/Temp/led_strip.cpp b/temporary/Temp/led_strip.cpp new file mode 100644 index 0000000..328c197 --- /dev/null +++ b/temporary/Temp/led_strip.cpp @@ -0,0 +1,564 @@ +#include "led_strip.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include + +#include +#include +#include "common/led_animation.h" +#include "common/LEDStrip.h" +#include "global.h" +#include "common/neo_colors.h" +#include "common/HSVTable.h" +#include "JsonConstrain.h" +#include "my_board.h" +#include "luma_master.h" +#include "common/luma-stiks.h" + +#define STRIP1_PIN RGBLED1_Pin +#define STRIP2_PIN RGBLED2_Pin + +#define POWERUP_TIME 3000 + +static const char* tag = "led_strip"; + +ANIMATION_PROPS animProps; + +LEDSTRIP *strip1; +LEDSTRIP *strip2; +TaskHandle_t Strip1_Task_Handle; +TaskHandle_t Strip2_Task_Handle; +TaskHandle_t FrontLight_Task_Handle; + +QueueHandle_t eventQueue = xQueueCreate( 4, sizeof( ANIMATION_EVENT ) ); +QueueHandle_t whiteQueue = xQueueCreate( 4, sizeof( ANIMATION_EVENT ) ); + +ANIMATION_EVENT lastNormalEventMsg; +ANIMATION_EVENT eventMsg; + +LUMA_PACKET TempLumaPacket; + +int CurrentRunningEventIndex = -1; + + +void Init_LED_Devices(BOARD_PINS boardPins) +{ + ESP_LOGD(tag, "Strips Initialization entered..."); + File file = LittleFS.open("/cfg/led-devices.json"); + + if(!file){ + ESP_LOGE(tag,"Error opening led-devices.json!"); + file.close(); + }else{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + if(error){ ESP_LOGE(tag, "led-devices.json deserialize error!.."); return;} + + // ********** STRIP1 ******************************** + JsonObject stripJson = doc["strip1"]; + if(jsonConstrainBool(tag, stripJson, "en", false)){ + int port = jsonConstrain(tag, stripJson, "i2s-ch", 0, 1, 0); + int size = jsonConstrain(tag, stripJson, "size", 1, 200, 10); + //int pin = jsonConstrainInt(stripJson, "pin", 0, 48, RGBLED1_Pin); + LED_ORDER order= getRGBOrder( jsonConstrainString(tag, stripJson, "rgb-order", "grb").c_str() ); + int shift = jsonConstrain(tag, stripJson, "shift", -size, size, 0); + int offset = jsonConstrain(tag, stripJson, "offset", -size, size, 0); + int core = jsonConstrain(tag, stripJson, "core", 0, 1, 0); + int powerDiv = jsonConstrain(tag, stripJson, "power-div", 0, 3, 0); + + strip1 = new LEDSTRIP(port, size, boardPins.rgb1, order, shift, offset); + strip1->setPowerDiv(powerDiv); + ESP_LOGI(tag,"Strip1 initialized."); + ESP_LOGD(tag," Size: %d, port: %d, shift: %d, offset: %d", size, port, shift, offset); + ESP_LOGD(tag," Strip1 Task Creating...Core: %d", core); + xTaskCreatePinnedToCore(Strip1_Control_Task, "LED_Strip1_Task", 1024*10, NULL, 1, &Strip1_Task_Handle, core); + } + + // ********** STRIP2 ******************************** + stripJson.clear(); + stripJson = doc["strip2"]; + if(jsonConstrainBool(tag, stripJson, "en", false)){ + int port = jsonConstrain(tag, stripJson, "i2s-ch", 0, 1, 0); + int size = jsonConstrain(tag, stripJson, "size", 1, 200, 10); + //int pin = jsonConstrainInt(stripJson, "pin", 0, 48, RGBLED2_Pin); + LED_ORDER order= getRGBOrder( jsonConstrainString(tag, stripJson, "rgb-order", "grb").c_str() ); + int shift = jsonConstrain(tag, stripJson, "shift", -size, size, 0); + int offset = jsonConstrain(tag, stripJson, "offset", -size, size, 0); + int core2 = jsonConstrain(tag, stripJson, "core", 0, 1, 0); + int powerDiv = jsonConstrain(tag, stripJson, "power-div", 0, 3, 0); + + strip2 = new LEDSTRIP(port, size, boardPins.rgb2, order, shift, offset); + strip2->setPowerDiv(powerDiv); + ESP_LOGI(tag,"Strip2 initialized."); + ESP_LOGD(tag," Size: %d, port: %d, shift: %d, offset: %d", size, port, shift, offset); + ESP_LOGD(tag,"Strip2 Task Creating...Core: %d", core2); + xTaskCreatePinnedToCore(Strip2_Control_Task, "LED_Strip2_Task", 1024*8, NULL, 1, &Strip2_Task_Handle, core2); + }else{ + ESP_LOGE(tag,"Strip2 disabled"); + } + + JsonObject flightJson = doc["front-light"]; + animProps.frontLight.enabled = jsonConstrainBool(tag, flightJson, "en", false); + if(animProps.frontLight.enabled){ + animProps.frontLight.relayIndex = jsonConstrain(tag, flightJson, "relay", 0, 3, 0); + animProps.frontLight.core = jsonConstrain(tag, flightJson, "core", 0, 1, 1); + animProps.frontLight.style = (FRONT_LIGHT_STYLE)(flightJson, "style", 0, 1, 0); + ESP_LOGI(tag, "Front Light initialized.."); + Init_FrontLight(); + } + + JsonObject rlightJson = doc["rear-light"]; + animProps.rearLight.enabled = jsonConstrainBool(tag, rlightJson, "en", false); + if(animProps.rearLight.enabled){ + animProps.rearLight.relayIndex = jsonConstrain(tag, rlightJson, "relay", 0, 3, 1); + animProps.rearLight.buttonIndex = jsonConstrain(tag, rlightJson, "button", 0, 2, 2); + animProps.rearLight.rampTime = jsonConstrain(tag, rlightJson, "ramp", 1000, 20000, 3000); + animProps.rearLight.rampSteps = jsonConstrain(tag, rlightJson, "steps", 2, 20, 8); + animProps.rearLight.min = jsonConstrain(tag, rlightJson, "min", 0.0f, 50.0f, 0.0f); + animProps.rearLight.max = jsonConstrain(tag, rlightJson, "max", animProps.rearLight.min, 100.0f, 100.0f); + ESP_LOGD(tag, "relay: %d, btn: %d, ramp: %d, steps: %d, min: %.2f, max: %.2f", animProps.rearLight.relayIndex, animProps.rearLight.buttonIndex, animProps.rearLight.rampTime, animProps.rearLight.rampSteps, animProps.rearLight.min, animProps.rearLight.max); + + Initialize_Rear_Control(animProps.rearLight.relayIndex, animProps.rearLight.buttonIndex, animProps.rearLight.rampTime, animProps.rearLight.rampSteps , animProps.rearLight.min, animProps.rearLight.max); + ESP_LOGI(tag, "Rear Light initialized.."); + } + + } + +} + +void Init_FrontLight(void) +{ + xTaskCreatePinnedToCore(FrontLight_Task, "FrontLight_Task", 5000, NULL, 1, &FrontLight_Task_Handle, animProps.frontLight.core); +} + +void Init_RearLight(void) +{ + //if(jsonOb.isNull()){ Serial.println(" rear_light json is Null.."); return; } +} + +void Test_Animations(void){ + ANIMATION_EVENT event; + + strip1->fill({0,0,0}, 0, strip1->effSize); + strip1->show(true); + vTaskDelay(100); + + event.hue = RGBtoHUE(col_blue); + event.hueRange = 180; + event.param1 = 11; + event.speed = 70; + + LEDStrip_FireInit(*strip1); + + while(1){ + event.hue = 0; + event.hueRange = 359; + event.param1 = 22; + event.speed = 70; + //Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2); + event.param1 = 30; + //Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2); + event.param1 = 40; + //Animation_Sectors(*strip1, event, true, DT_BOTH, true, 6, 2); + event.param1 = 50; + Animation_Sectors(*strip1, event, DT_BOTH, 2); + + event.param1 = 50; + Animation_Dashes(*strip1, event, DT_BOTH, 3); + + event.param1 = 50; + Animation_Dashes(*strip1, event, DT_BOTH, 3); + /* + Animation_Fire(*strip1, event, RED_FIRE, 5000); + Animation_Fire(*strip1, event, GREEN_FIRE, 5000); + Animation_Fire(*strip1, event, BLUE_FIRE, 5000); + + event.col1 = col_blue; + event.col2 = {255,136,0}; + event.density = 11; + event.speed = 100; + Animation_Snakes(*strip1, event, false, DT_FWD, 2, 1); + + event.col1 = HUEtoRGB(359); + event.col2 = HUEtoRGB(0); + event.density = 22; + event.speed = 70; + Animation_Snakes(*strip1, event, true, DT_BOTH, 12, 4); + + event.col1 = HUEtoRGB(359); + event.col2 = HUEtoRGB(0); + event.density = 56; + event.speed = 50; + Animation_Snakes(*strip1, event, true, DT_BOTH, 12, 4); + */ + } +} + +void Strip1_Control_Task(void *parameters) +{ + vTaskDelay(1000); // small start delay + ESP_LOGD(tag, "Strip1 Task Entered...."); + //Test_Animations(); + + LEDStrip_FireInit(*strip1); // must be initialized before calling fire animation + + // Set default event 1 animation base on profile values + ANIMATION_EVENT ev; // empty event + ev.animIndex = 0; + ev.countDown = 6000; + ev.param2 = 75; + ev.type = EV_NON_INJECT; + PostNewEvent(ev); // Start Animation (White Fill) + + animProps.event[1].type = EV_NORMAL; + PostNewEvent(animProps.event[1]); // default attraction animation + + while(true){ + xQueueReceive( eventQueue, &eventMsg, portMAX_DELAY ); + ESP_LOGD(tag, "Queue waiting: %d", uxQueueMessagesWaiting(eventQueue)); + + if(eventMsg.type == EV_NORMAL ) { // EV_NORMAL + ESP_LOGD(tag, "Running event: type: EV_NORMAL, event: %d, anim: %d", eventMsg.selfIndex, eventMsg.animIndex); + lastNormalEventMsg = eventMsg; //save last Normal EventMsg + if( eventMsg.selfIndex >= 0 ) { + CurrentRunningEventIndex = eventMsg.selfIndex; + } + + if(Luma_PeerCount() > 0){ + TempLumaPacket.type = LPT_ANIM; + memcpy(&TempLumaPacket.data, &eventMsg, sizeof(ANIMATION_EVENT)); + Luma_Master_Broadcast(TempLumaPacket); + } + + if( eventMsg.selfIndex == 0 ){ + RunWhitefillAnimation( eventMsg ); + }else{ + RunAnimation( eventMsg ); + } + } + else if( eventMsg.type == EV_INJECT ){ // insert and return to previous animation + ESP_LOGD(tag, "EV_INJECT"); + xQueueSend( eventQueue, &lastNormalEventMsg, 100 ); // requeue previous normal event + RunPopUpAnimations( eventMsg ); + } + else if( eventMsg.type == EV_NON_INJECT ){ + ESP_LOGD(tag, "EV_NON_INJECT"); + RunPopUpAnimations( eventMsg ); + } + else if( eventMsg.type == EV_TEST ){ + ESP_LOGD(tag, "EV_TEST"); + animStatus.EventTestCountdown = EVENT_TESTMODE_TIMEOUT; // start timeout countdown + // Should not re-post last event from here in case repeated tests are started. + // that would cause a backlog of the same previous event. + if( eventMsg.selfIndex == 0 ){ + RunWhitefillAnimation( eventMsg); + }else{ + RunAnimation( eventMsg ); + } + } + } +} + +void PostLastNormalEvent(){ + xQueueSend( eventQueue, &lastNormalEventMsg, 100 ); // requeue previous normal event + ESP_LOGD(tag, "RePosted Last Normal Event Index: %d", lastNormalEventMsg.selfIndex); +} + +// Used to cycle to the next event primarity when usinging buttons +int IncrementEventIndex(){ + int x = (CurrentRunningEventIndex + 1) % animStatus.EventsCount; + ESP_LOGD(tag, "Increment Event Index from %d to %d", CurrentRunningEventIndex, x); + + animProps.event[x].type = EV_NORMAL; + PostNewEvent(animProps.event[x]); + return animStatus.EventsIndex; +} + +//EVENT_MSG newEvent; +void PostNewEvent( ANIMATION_EVENT &animEvent) { + //newEvent.type = type; + //newEvent.event = animEvent; + xQueueSend(eventQueue, &animEvent, 100); // queue new event + + // if(type == EV_NORMAL){ + // If whitefill index ( could be restarted ) + // if(animEvent.animIndex == ANIM_WHITEFILL_INDEX){ + //if( countDown == 0 ) { animStatus.countDown = 1000; } // TODO Countdown calc maybe wrong...double check + //if( animStatus.busy ) { animStatus.repeat = true; } + // if( FrontLight_Task_Handle ) { xTaskNotifyGive( FrontLight_Task_Handle ); } + // } + // } + + if( Strip1_Task_Handle ){ xTaskNotifyGive( Strip1_Task_Handle ); } +} + +void Strip2_Control_Task(void *parameters) +{ + vTaskDelay(100); + ESP_LOGD(tag, "Strip2 Task Entered....\n"); + + for(;;){ + vTaskDelay(10000); + } +} + +void FrontLight_Task(void *parameters){ + animStatus.busy = false; + ANIMATION_EVENT whiteMsg; + for(;;){ + xQueueReceive( whiteQueue, &whiteMsg, portMAX_DELAY ); + + int msFillTime = 0; + if(whiteMsg.countDown > 0){ // use countdown if available + msFillTime = whiteMsg.countDown; + }else{ + msFillTime = map(whiteMsg.param1, 0, 100, 0, 10000); + if(msFillTime < 1000){ msFillTime = 1000; } + } + float delayFactor = (float)map(whiteMsg.param2, 0, 100, 0, 100) / 100.0; + Const_White_Fill(whiteStatus, animProps.frontLight, delayFactor, msFillTime, 50); + } +} + +void load_animation_profile(void){ + File file = LittleFS.open("/cfg/anim-profile-common.json"); + + if(!file){ + ESP_LOGE(tag, "Error opening anim-profile-common.json..."); + file.close(); + }else{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + if(error){ ESP_LOGE(tag, "anim-profile-common.json deserialize error!.."); return;} + + JsonObject frontJson = doc["countdown"]; + animProps.frontLight.minDuty = jsonConstrain(tag, frontJson, "min", 0.0f, 99.0f, 20.0f); + animProps.frontLight.maxDuty = jsonConstrain(tag, frontJson, "max", 1.0f, 100.0f, 99.0f); + animProps.frontLight.holdTime = jsonConstrain(tag, frontJson, "hold", 100, 5000, 1000); + animProps.frontLight.rampTime = jsonConstrain(tag, frontJson, "ramp", 100, 5000, 500); + + animProps.profileIndex = jsonConstrain(tag, doc.as(), "profile-index", 0, 7, 0); + + ESP_LOGI(tag, "anim-profile-common.json settings loaded."); + } +} + +void load_animation_profileByIndex(int index){ + + //for(int index = 1; index <= 8; index++){ + String strFileName = "/cfg/anim-profile" + String(index + 1) + ".json"; + File file = LittleFS.open(strFileName); + + if(!file){ + ESP_LOGE(tag, "%s", "Error opening " + strFileName); + file.close(); + }else{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + if(error){ ESP_LOGE(tag, "%s deserialize error!..", strFileName.c_str()); return;} + + JsonObject profJson = doc.as(); + animProps.profileName = jsonConstrainString(tag, profJson, "name", "").c_str(); + ESP_LOGD(tag, "Profile Index: %d, Name: %s", index, animProps.profileName); + + // + JsonArray eventsJson = profJson["events"]; + for(int i = 0; i < animStatus.EventsCount; i++){ + animProps.event[i].selfIndex = i; // Self Index + + if(i == 0){ + animProps.event[i].animIndex = jsonConstrain(tag, eventsJson[i], "anim", 0, animProps.whitefillsCount-1, 0); + }else{ + animProps.event[i].animIndex = jsonConstrain(tag, eventsJson[i], "anim", 0, animProps.animationsCount-1, 0); + } + + animProps.event[i].hue = jsonConstrain(tag, eventsJson[i], "hue", -2, 360, 0); + animProps.event[i].hueRange = jsonConstrain(tag, eventsJson[i], "hue-range", 0, 360, 1); + animProps.event[i].speed = jsonConstrain(tag, eventsJson[i], "speed", 0, 100, 50); + animProps.event[i].param1 = jsonConstrain(tag, eventsJson[i], "param1", 0, 100, 50); + animProps.event[i].param2 = jsonConstrain(tag, eventsJson[i], "param2", 0, 100, 50); + animProps.event[i].check1 = jsonConstrainBool(tag, eventsJson[i], "check1", false); + animProps.event[i].check2 = jsonConstrainBool(tag, eventsJson[i], "check2", false); + animProps.event[i].check3 = jsonConstrainBool(tag, eventsJson[i], "check3", false); + animProps.event[i].check4 = jsonConstrainBool(tag, eventsJson[i], "check4", false); + + ESP_LOGD(tag, " self: %d, anim: %d, param1: %d, speed: %d", animProps.event[i].selfIndex, animProps.event[i].animIndex, animProps.event[i].param1, animProps.event[i].speed); + } + ESP_LOGI(tag, "%s settings loaded.", strFileName.c_str()); + } + //} +} + +void RunWhitefillAnimation(ANIMATION_EVENT &event){ + //Start Constant Light Task if available + if( FrontLight_Task_Handle ){ + xQueueSend(whiteQueue, &event, 100); // queue new event + xTaskNotifyGive( FrontLight_Task_Handle ); + } + + int ret; + switch(event.animIndex){ + case 0: // Bottom/Up Fill + ESP_LOGD(tag, " Running WhiteFill: %d , bottom/up", event.animIndex); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + case 1: // Snake Fill + ESP_LOGD(tag, " Running WhiteFill: %d , Snake fill", event.animIndex); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + case 2: // Tick Fill + ESP_LOGD(tag, " Running WhiteFill: %d , Tick fill", event.animIndex); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + case 3: // Smooth Brighten + ESP_LOGD(tag, " Running WhiteFill: %d , Smooth Brighten", event.animIndex); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + case 4: // Cycle All + ESP_LOGD(tag, " Running WhiteFill: %d , Cycle All", event.animIndex); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + default: // Random Select + ESP_LOGD(tag, " Running WhiteFill: default , bottom/up"); + ret = Animation_White_Fill_Mirrored(*strip1, event); + break; + } + + // if exited loop naturally... not from a notification so keep waiting + if(ret == EXIT_FINISHED || ret == EXIT_TIMEOUT){ + ESP_LOGD(tag, "Whitefill normal exit... waiting"); + ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); + } +} + +void RunAnimation(ANIMATION_EVENT &event){ + if(event.animIndex < 80){ + switch(event.animIndex){ // AnimationEventIndex (0-X) + case 0: // Rainbow + ESP_LOGD(tag, " Running Anim: %d , Rainbow", event.animIndex); + Animation_Rainbow(*strip1, event, INFINITE_LOOP); + break; + case 1: // Hue Spectrum Mirrored + ESP_LOGD(tag, " Running Anim: %d , Hue Spectrum Mirrored", event.animIndex); + Animation_Hue_Spectrum_Mirrored(*strip1, event, INFINITE_LOOP); + break; + case 2: // Color Pulse Cycle + ESP_LOGD(tag, " Running Anim: %d , Color Pulse Cycle", event.animIndex); + Animation_Pulse_Color_Cycling(*strip1, event); + break; + case 3: // Comets Mixed Colors + ESP_LOGD(tag, " Running Anim: %d , Comets", event.animIndex); + Animation_Comets(*strip1, event, DT_BOTH, INFINITE_LOOP); + break; + case 4: // Dashes Single and Mixed Colors + ESP_LOGD(tag, " Running Anim: %d , Dashes", event.animIndex); + Animation_Dashes(*strip1, event, DT_BOTH, INFINITE_LOOP); + break; + case 5: // Snakes Single and Mixed Colors + ESP_LOGD(tag, " Running Anim: %d , Snakes", event.animIndex); + Animation_Snakes(*strip1, event, DT_BOTH, INFINITE_LOOP); + break; + case 6: // Sectors Mixed Colors (No Sigle Colors because pointless) + ESP_LOGD(tag, " Running Anim: %d , Sectors", event.animIndex); + Animation_Sectors(*strip1, event, DT_BOTH, INFINITE_LOOP); + break; + case 7: // Fire (Red) + ESP_LOGD(tag, " Running Anim: %d , Fire", event.animIndex); + Animation_Fire(*strip1, event); + break; + case 8: // + break; + case 9: // Color Strobing + break; + case 10: // Random Color Twinkle + break; + case 11: // Stacking + break; + case 12: // Rain + break; + case 13: // Stacking + break; + case 14: // RED/WHITE/BLU Flag (density selects color pallet) (sector/comets,dashes, snakes) + break; + case 15: + break; + default: + break; + } + }else{ // Non Looping Functions + switch(event.animIndex){ // AnimationEventIndex (0-X) + case 80: // clear strip + strip1->fill(col_black, 0, strip1->effSize); + strip1->show(true); + ESP_LOGD(tag, "Animation Strip1 Clear"); + ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); + break; + case 81: // set pixel + strip1->setPixel(event.param1, GetColorFromHue(event.hue)); + strip1->show(true); + ESP_LOGD(tag, "Animation Set Pixel: %d", event.param1); + ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); + break; + case 82: // + break; + default: + break; + } + } + +} + +// These animations should be quick and not infinite +void RunPopUpAnimations(ANIMATION_EVENT &event){ + + switch(event.animIndex){ // AnimationEventIndex (0-X) + case 0: // Start/Boot/Power Up Sequence + ANIMATION_EVENT ev; + ev.animIndex = 0; + ev.hue = -1; //{200, 200, 200}; //col_white; + ev.hueRange = 1; //col_black; + ev.countDown = 6000; // 6 seconds + ev.param2 = 75; + if( FrontLight_Task_Handle ){ + xQueueSend(whiteQueue, &ev, 100); // queue new event + xTaskNotifyGive( FrontLight_Task_Handle ); + } + Animation_White_Fill_Mirrored(*strip1, ev); + + break; + case 1: // BLE Connected + break; + case 2: // BLE Disconnected + break; + case 3: // WiFi Connected + break; + case 4: // WiFi Disconnected + break; + case 5: // Luma Stick Connected + break; + default: + break; + } + +} + + + +// json color to rgbPixel conversion +// Unused now +rgbpixel_t hexToRGB(const char* hexColor) { + long hexValue = strtol(hexColor + 1, nullptr, 16); // Skip the '#' character + rgbpixel_t pix; + pix.red = (hexValue >> 16) & 0xFF; + pix.grn = (hexValue >> 8) & 0xFF; + pix.blu = hexValue & 0xFF; + return pix; +} + + + + diff --git a/temporary/Temp/led_strip.h b/temporary/Temp/led_strip.h new file mode 100644 index 0000000..3f363fa --- /dev/null +++ b/temporary/Temp/led_strip.h @@ -0,0 +1,128 @@ +#ifndef _LED_STRIP_H +#define _LED_STRIP_H + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "common/LEDStrip.h" +#include +#include "my_board.h" + +extern LEDSTRIP *strip1; +extern LEDSTRIP *strip2; +extern TaskHandle_t Strip1_Task_Handle; +extern TaskHandle_t Strip2_Task_Handle; +extern TaskHandle_t FrontLight_Task_Handle; + +#define ANIM_WHITEFILL_INDEX 0 +#define ANIM_DEFAULT_INDEX 1 +#define ANIM_EVENT_SIZE 12 +#define PROFILES_SIZE 8 + +#define MIN_LED_UPDATE_INTERVAL 13 + +enum OtherANIMS { OA_SET_PIXEL=100, OA_CLEAR, }; + +enum FRONT_LIGHT_STYLE { FRONT_LIKE_LUMIA, FRONT_LIKE_ROAMER }; + +enum EVENT_TYPE { EV_NORMAL, EV_INJECT, EV_NON_INJECT, EV_TEST,}; + +// Single web page event (upto 8) +typedef struct{ + EVENT_TYPE type; + int selfIndex; + int animIndex; + int hue; + int hueRange; + int speed; + int param1; + int param2; + bool check1; + bool check2; + bool check3; + bool check4; + int countDown; +}ANIMATION_EVENT; + +typedef struct{ + bool enabled; + uint8_t relayIndex; + uint8_t core; + int holdTime; + int rampTime; + float minDuty; + float maxDuty; + FRONT_LIGHT_STYLE style; + //float delayFactor; +}FRONT_LIGHT; + +typedef struct{ + bool enabled; + uint8_t relayIndex; + uint8_t buttonIndex; + uint8_t core; + int rampTime; + uint8_t rampSteps; + float min; + float max; +}REAR_LIGHT; + + +typedef struct { + EVENT_TYPE type; + ANIMATION_EVENT event; +}EVENT_MSG; + +// Properties from webpage +typedef struct { + int profileIndex; + const char* profileName; + int whitefillsCount; + int animationsCount; + FRONT_LIGHT frontLight; + REAR_LIGHT rearLight; + ANIMATION_EVENT event[ANIM_EVENT_SIZE]; +}ANIMATION_PROPS; + +extern ANIMATION_PROPS animProps; + +enum POPUP_ANIM { POP_STATUP, POP_BLE_CONN, POP_BLE_DISC, POP_WIFI_CONN, POP_WIFI_DISC, POP_STICK_CONN, POP_STICK_DISC }; + +void RunWhitefillAnimation(ANIMATION_EVENT &event); + +void RunAnimation(ANIMATION_EVENT &event); + +void RunPopUpAnimations(ANIMATION_EVENT &event); + +//void SetEventIndex(int index, int countdown=0, bool test=false); + +//void PostNewEvent( int countDown, EVENT_TYPE type, ANIMATION_EVENT &animEvent); +void PostNewEvent( ANIMATION_EVENT &animEvent); + +int IncrementEventIndex(void); + +void PostLastNormalEvent(void); + +void load_animation_profile(void); // cfg/anim-profiles.json +void load_animation_profileByIndex(int); // cfg/anim-profileX.json + +void Init_LED_Devices(BOARD_PINS); // cfg/led-devices.json + +rgbpixel_t hexToRGB(const char* hexColor); + +//void Init_LEDStrip2(void); + +void Init_FrontLight(void); + +//void Init_RearLight(void); + +void Strip1_Control_Task(void *parameters); + +void Strip2_Control_Task(void *parameters); + +void FrontLight_Task(void *parameters); + + + + + +#endif diff --git a/temporary/Temp/luma_master.cpp b/temporary/Temp/luma_master.cpp new file mode 100644 index 0000000..7ec642a --- /dev/null +++ b/temporary/Temp/luma_master.cpp @@ -0,0 +1,111 @@ +#include "luma_master.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include +#include +#include "JsonConstrain.h" +#include +#include +#include "global.h" + +static const char* tag = "lumaM"; +uint8_t broadcastAddress[] = {0xFF, 0xFF,0xFF,0xFF,0xFF,0xFF}; +uint8_t availableStiks = 0; +LUMA_NODE lumaStation[LUMA_NODE_MAX_COUNT]; +bool LUMASTIKS_READY = false; + +TaskHandle_t LumaMaster_Task_Handle; + +bool SendRegistrationPacket = false; +LUMA_PACKET RegistrationPacket; +esp_now_peer_info_t TempNode; + + + +QueueHandle_t lumaQueue = xQueueCreate( 4, sizeof( LUMA_PACKET ) ); + +void Init_Luma_Master(void){ + + if (esp_now_init() != ESP_OK) { + ESP_LOGD(tag, "Error initializing ESP-NOW\n"); + return; + } + + xTaskCreatePinnedToCore(LumaMaster_Task, "LumaMaster_Task", 1024*6, NULL, 1, &LumaMaster_Task_Handle, CONFIG_ARDUINO_RUNNING_CORE); + ESP_LOGV(tag, "Initialized LumaMaster task created..."); + + esp_now_register_recv_cb(Luma_Master_Data_Received); + esp_now_register_send_cb(Luma_Master_Data_Sent); +} + +void LumaMaster_Task(void *parameters){ + + while(1){ + vTaskSuspend(NULL); + + if(SendRegistrationPacket){ + Luma_Send_Packet(TempNode.peer_addr, RegistrationPacket); + } + } + + + LUMA_PACKET lumaPacket; + + while(1){ + xQueueReceive( lumaQueue, &lumaPacket, portMAX_DELAY ); + + switch(lumaPacket.type){ + case LPT_RAW: + break; + case LPT_REG: + Luma_Send_Packet(TempNode.peer_addr, lumaPacket); + break; + case LPT_ANIM: + Luma_Master_Broadcast(lumaPacket); + break; + default: + break; + } + } +} + +void Luma_Master_Data_Sent(const uint8_t *mac_addr, esp_now_send_status_t status){ + +} + +void Luma_Master_Data_Received(const uint8_t *mac, const uint8_t *data, int len){ + + LUMA_PACKET *packet = (LUMA_PACKET*)data; + + printf("** Data Received **\n\n"); + printf("Received from MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + printf("Length: %d byte(s)\n", len); + printf("Data: %d\n\n", data[0]); + + switch(packet->type){ + case LPT_RAW: + break; + case LPT_REG: + LUMA_REGISTER_PACKET *reg = (LUMA_REGISTER_PACKET*)&packet->data; + ESP_LOGD(tag, "Node SSID: %s", reg->ssid); + + // Register + memcpy(TempNode.peer_addr, mac, 6); + TempNode.channel = WiFi.channel(); + Luma_Add_Peer(TempNode); + + // Notify Peer + RegistrationPacket.type = LPT_REG; + xQueueSend( lumaQueue, &RegistrationPacket, 100 ); + break; + //default: + // break; + } +} + +void Luma_Master_Broadcast(LUMA_PACKET packet){ + +} + + diff --git a/temporary/Temp/luma_master.h b/temporary/Temp/luma_master.h new file mode 100644 index 0000000..5808f26 --- /dev/null +++ b/temporary/Temp/luma_master.h @@ -0,0 +1,24 @@ +#ifndef _LUMA_MASTER_H +#define _LUMA_MASTER_H + +#include +#include +#include "common/luma-stiks.h" + + +extern bool LUMASTIKS_READY; + +#define LUMA_NODE_MAX_COUNT 16 +//extern LUMA_NODE lumaStation[]; + +void LumaMaster_Task(void *parameters); + +void Init_Luma_Master(void); + +void Luma_Master_Data_Received(const uint8_t *mac, const uint8_t *data, int len); + +void Luma_Master_Data_Sent(const uint8_t *mac_addr, esp_now_send_status_t status); + +void Luma_Master_Broadcast(LUMA_PACKET packet); + +#endif \ No newline at end of file diff --git a/temporary/Temp/my_wifi.cpp b/temporary/Temp/my_wifi.cpp new file mode 100644 index 0000000..12ec314 --- /dev/null +++ b/temporary/Temp/my_wifi.cpp @@ -0,0 +1,1451 @@ + +/* + wifi is started in STATION mode and tries to connected to AP saved + in the creds.json file. It will keep trying to connect forever. If + it connects then the board LEDS will toggle, the buzzer will play a tune + and the IP address will be sent to the serial port. Also the device will + be discoverable as "http://atadev.local/" + + If credentials need to be changed then double reset can be done to + trigger the AP mode and a wifi config page will be available on address + 192.168.4.1. Credentials can be entered and applied then you can reset + the device so it connects with the correct credentials. + +*/ +#include "my_wifi.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "global.h" +#include +#include "my_buzzer.h" +#include "my_board.h" +#include "common/led_animation.h" +#include "led_strip.h" +#include "common/fileSystem.h" +#include "JsonConstrain.h" +#include "led_strip.h" +#include "my_oled.h" +#include "BTSerial.h" +#include "esp_log.h" + +#include "firmware_html.h" + +int RebootSystem = 0; +#define WIFI_TIMEOUT_MS 10000 + +static const char* tag = "wifi"; + +bool WifiClientConnected = false; +AsyncWebServer webServer(80); +DNSServer *dnsServer; +#define DNS_PORT 53 + +#define StartDelayedRebooth RebootSystem = 1000/BUTTON_UPDATE_PERIOD; // start reboot countdown for 1sec + +String ssid; +String passPhrase; + +String AP_SSID; +String AP_Pass; +String mDnsName; +String HostName; + +IPAddress local_IP(192,168,10,1); +IPAddress gateway(192,168,10,200); +IPAddress subnet(255,255,255,0); + +// for file manager page +String filesDropdownOptions((char*)0); +String dirDropdownOptions((char*)0); +String savePath((char*)0); // needed for storing file when editing a file +String savePathInput((char*)0); +const char* http_username = "admin"; +const char* http_password = "admin"; +const char* param_delete_path = "delete-path"; +const char* param_edit_path = "edit-path"; +const char* param_dir_pad = "dir-path"; +const char* param_edit_textarea = "edit-textarea"; +const char* param_save_path = "save-path"; +String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg"; +//const char* jquery = "/jquery-3.6.3.min.js"; + +void Init_Wifi_Task(void) +{ + File file = LittleFS.open("/cfg/wifi.json", "r"); + if(!file){ + ESP_LOGE(tag, "Failed to open wifi.json!"); + file.close(); + return; + } + + // Parse the JSON file + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + if(!error){ + JsonObject wifiJson = doc["wifi"]; + file.close(); + mDnsName = jsonConstrainString(tag, wifiJson,"mdns-name", "atadev"); + ESP_LOGD(tag, "mDnsName: %s", mDnsName.c_str()); + + if(jsonConstrainBool(tag, wifiJson, "en", false)){ + JsonObject j = doc["cred1"]; + Wifi_Read_Credentials( j ); + xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*10, NULL, 1, NULL, CONFIG_ARDUINO_RUNNING_CORE); + ESP_LOGD(tag, "Initialized WiFi (task created)..."); + } + + JsonObject ap = doc["ap"]; + AP_SSID = jsonConstrainString(tag, ap, "ssid", "ATA_AP"); + if(jsonConstrainBool(tag, ap, "append-id", true)){ + String macHex = String(chipInfo.macByte[1], HEX) + String(chipInfo.macByte[0], HEX); + AP_SSID += "_"; + macHex.toUpperCase(); + AP_SSID += macHex; + } + AP_Pass = jsonConstrainString(tag, ap, "pass", "12345678"); + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } +} + +void onWiFiEvent(WiFiEvent_t event) +{ + //Serial.printf("[WiFi-event] event: %d\n", event); + switch (event) { + case ARDUINO_EVENT_WIFI_READY: + //Serial.println("WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + //Serial.println("Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + //Serial.println("WiFi client started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + //Serial.println("WiFi clients stopped"); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + WifiClientConnected = true; + //Serial.println("Connected to access point"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + WifiClientConnected = false; + ESP_LOGI(tag,"WiFi Disconnected"); + //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + //Serial.println("Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + ESP_LOGI(tag,"My IP: %s", WiFi.localIP().toString()); + Wifi_Start_MDNS(); + Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + //Serial.println("Lost IP address and IP address is reset to 0"); + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + //Serial.println("WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + //Serial.println("WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + // Serial.println("WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + //Serial.println("WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + //Serial.println("WiFi access point started"); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + //Serial.println("WiFi access point stopped"); + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + //Serial.println("Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + ESP_LOGI(tag,"SoftAP Client Disconnected"); + //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + ESP_LOGI(tag,"SoftAP Client Connected"); + Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + //Serial.println("Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + //Serial.println("AP IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: + //Serial.println("STA IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_GOT_IP6: + //Serial.println("Ethernet IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_START: + //Serial.println("Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + //Serial.println("Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + //Serial.println("Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + //Serial.println("Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + // Serial.println("Obtained IP address"); + break; + default: break; + } +} + +void Wifi_Task(void *parameters) +{ + // Extend watchdog timer + esp_task_wdt_init(2, false); + + Setup_WebServer_Handlers(&webServer); + WiFi.onEvent(onWiFiEvent); + WiFi.setHostname(mDnsName.c_str()); + WiFi.softAPConfig(local_IP, gateway, subnet); + WiFi.softAP(AP_SSID.c_str(), AP_Pass.c_str()); + Wifi_Start_MDNS(); + vTaskDelay(100); + webServer.begin(); + + // Choose WIFI Mode + if(commMode == COMM_WIFI_AP_BLE){ // AP Only + WiFi.mode(WIFI_AP); + while(1){ vTaskDelay(500 / portTICK_PERIOD_MS); } + }else{ // AP & Client (dslrbooth) + esp_wifi_set_ps(WIFI_PS_NONE); + WiFi.mode(WIFI_AP_STA); + Wifi_Client_Loop(); + } +} + +void Wifi_Client_Loop(){ + while(true){ + // Wifi status check loop + if(WiFi.status() == WL_CONNECTED){ + vTaskDelay(250); + continue; + } + + // Start WiFi Client + ESP_LOGD(tag, "Wifi trying stored creds: %s..%s",ssid.c_str(), passPhrase.c_str()); + WiFi.begin(ssid.c_str(), passPhrase.c_str()); + + //Wait until connected or timeout + ESP_LOGV(tag, "WiFi Connecting..."); + unsigned long startAttemptTime = millis(); + while(WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS){ + vTaskDelay(100 / portTICK_PERIOD_MS); + } + + // Delay if wifi connection fails and continue + if(WiFi.status() != WL_CONNECTED){ + ESP_LOGW(tag, "WIFI Connection Failed!"); + for(int i = 0; i < WIFI_TIMEOUT_MS/100; i++){ + vTaskDelay(100); + } + continue; + } + } +} + +void Wifi_Start_MDNS(void) +{ + ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str()); + if (!MDNS.begin(mDnsName.c_str())) { + ESP_LOGE(tag, "Error setting up MDNS responder!"); + }else{ + ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName); + } +} + +void Wifi_Read_Credentials(const JsonObject &credJson){ + if(credJson.isNull()){ // set generic defaults on error + ESP_LOGE(tag, "Failed to find wifi key"); + + ssid = "Generic"; + passPhrase = "password"; + //strncpy(ssid, "Generic", sizeof(ssid)-1); + //strncpy(passPhrase, "password", sizeof(passPhrase)-1); + return; + } + else{ + // TODO Restore String + ssid = jsonConstrainString(tag, credJson, "ssid", "default"); + passPhrase = jsonConstrainString(tag, credJson, "pass", "password"); + //strncpy(ssid, jsonConstrainString(tag, credJson, "ssid", "default").c_str(), sizeof(ssid)-1); + //strncpy(passPhrase, jsonConstrainString(tag, credJson, "pass", "password").c_str(), sizeof(passPhrase)-1); + } +} + +void Wifi_Save_Credentials(String newSSID, String newPass) +{ + // Open the credentials file writing + File credsFile = LittleFS.open("/cfg/wifi.json", "r+"); + if(!credsFile){ + ESP_LOGE(tag, "Failed to open wifi.json\n"); + return; + } + + // Parse the JSON file + JsonDocument doc; + DeserializationError error = deserializeJson(doc, credsFile); + if(!error){ + JsonObject cred1Json = doc["cred1"].to(); + + if(cred1Json){ + cred1Json["ssid"] = newSSID; + cred1Json["pass"] = newPass; + + // Clear file contents before saving the modified JSON + credsFile.seek(0); + + int written = serializeJson(doc, credsFile); // save to file + ESP_LOGD(tag, "Credentials saved... ssid: %s, pass: %s, size written: %d", newSSID, newPass, written); + } + credsFile.close(); + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } +} + +void Setup_WebServer_Handlers(AsyncWebServer* serv) +{ + serv->on("/dslrbooth", HTTP_GET, handleGET_DSLRBooth); + + serv->on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_AP && !WifiClientConnected){ + request->redirect("/wifi"); + }else{ + request->redirect("/home"); + } + }); + serv->on("/home", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); + }); + serv->on("/lights", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/lights.html", request, htmlProcessor); + }); + serv->on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/setup.html", request, htmlProcessor); + }); + serv->on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_MODE_APSTA){ + // TODO Disable navigation bar + } + sendHtmlFile("/www/wifi.html", request, htmlProcessor); + }); + serv->on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/files.html", request, htmlProcessor); + }); + serv->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handlePOST_Upload); + + serv->on("/download", HTTP_GET, [](AsyncWebServerRequest *request){ + if (request->hasParam("file")) { + String filename = request->getParam("file")->value(); + if (LittleFS.exists(filename)) { + fs::File file = LittleFS.open(filename, "r"); + if (file) { + request->send(LittleFS, filename, "application/octet-stream"); + file.close(); + return; + } + } + } + request->send(404, "text/plain", "File not found!"); + }); + serv->on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + + String filename = request->getParam(param_delete_path)->value(); + if(filename !="choose"){ + LittleFS.remove(filename.c_str()); + ESP_LOGD(tag, "Deleted file: %s", filename.c_str()); + } + + request->redirect("/files"); + }); + serv->on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + + String fileName = request->getParam(param_edit_path)->value(); + if(fileName =="new"){ + savePath = "/new.txt"; + } + else{ + savePath = fileName; + } + + ESP_LOGD(tag, "Edit file: %s", savePath.c_str()); + sendHtmlFile("/www/edit.html", request, htmlProcessor); + }); + serv->on("/save", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + String inputMessage((char*)0); + if (request->hasParam(param_edit_textarea)) { + inputMessage = request->getParam(param_edit_textarea)->value(); + } + if (request->hasParam(param_save_path)) { + savePath = request->getParam(param_save_path)->value(); + } + writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); + + request->redirect("/files"); + }); + serv->on("/update", HTTP_POST, handlePOST_Update, updateCallback); + + // Request Data + serv->on("/get", HTTP_GET, handleGET_Get); + // Post Data + serv->on("/post", HTTP_POST, handlePOST_Post, postFileUpload, postBody); + + serv->on("/generate_204", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/generate_204 ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/hotspot-detect.html", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/hotspot-detect.html ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/msftconnecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/msftconnecttest.txt ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/reg-stick", HTTP_POST, handlePOST_RegStick); + + serv->on("/firmware", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", firmware_html_page); + }); + + serv->onNotFound([](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_MODE_APSTA){ + ESP_LOGD(tag, "OnNotFound Redirect to /wifi"); + request->redirect("/wifi"); + }else if(WiFi.getMode() == WIFI_MODE_AP){ + ESP_LOGD(tag, "OnNotFound Redirect to /home"); + request->redirect("/home"); + }else{ + request->send(404); + } + }); + + serv->on("/*", HTTP_GET, handleGET_SendFile); // send any file + +} + +void handlePOST_RegStick(AsyncWebServerRequest *request){ + +} + +bool getpostSuccess = false; +// +void handleGET_Get(AsyncWebServerRequest *request){ + if(request->hasParam("type", false, false)) { + const char* dataType = request->getParam("type", false, false)->value().c_str(); + + if(!strcmp(dataType, "app-events")){ // send json file + request->send(LittleFS, "/cfg/app-events.json", "application/json"); + return; + } + + if(!strcmp(dataType, "anim-profiles")){ // send json file + request->send(LittleFS, "/cfg/anim-profiles.json", "application/json"); + return; + } + + if(!strcmp(dataType, "anim-list")){ + request->send(LittleFS, "/cfg/anim-list.json", "application/json"); + return; + } + + if(!strcmp(dataType, "sys-summary")){ + JsonDocument doc; + CreateSysSummmaryPacket(doc); + String summary; + serializeJson(doc, summary); + request->send(200, "application/json", summary); + return; + } + + if(!strcmp(dataType, "wifi")){ + JsonDocument jsdoc; + jsdoc["ssid"] = ssid; + //jsdoc["pass"] = (int)strlen(passPhrase); + jsdoc["pass"] = passPhrase; + + String jsStr; + serializeJson(jsdoc, jsStr); + + request->send(200, "application/json", jsStr); + return; + } + } + + request->send(400, "text/plain", "Failed"); +} + +void CreateSysSummmaryPacket(JsonDocument &doc){ + JsonObject booth = doc["booth"].to(); + booth["mode"] = ""; + booth["app"] = sysProps.appName; + booth["strip1"] = (strip1) ? true : false, + booth["strip2"] = (strip2) ? true : false, + booth["front-light"] = true; + booth["rear-light"] = false; + booth["oled"] = (oled) ? true : false; + + //ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getHeapSize()-ESP.getFreeHeap()); + + JsonObject Temperature = doc["temperature"].to(); + Temperature["temp"] = sysProps.t_sensor.temperature; + Temperature["sp1"] = sysProps.t_sensor.Setpoint1; + doc["system"]["firm-ver"] = FIRMWARE_VER; + + JsonObject chip = doc["chip"].to(); + chip["flash-size"] = ""; + chip["flash-free"] = ""; + chip["ram-size"] = ESP.getHeapSize(); + chip["ram-free"] = ESP.getFreeHeap(); + + + + // Get the Wi-Fi RSSI + wifi_ap_record_t wifi_ap_info; + esp_err_t rssi_result = esp_wifi_sta_get_ap_info(&wifi_ap_info); + int rssi = 0; int wifi_ch = 0; + String encryp = ""; + if (rssi_result == ESP_OK) { + rssi = wifi_ap_info.rssi; + wifi_ch = wifi_ap_info.primary; + + wifi_auth_mode_t auth_mode = wifi_ap_info.authmode; + switch (auth_mode) { + case WIFI_AUTH_OPEN: + encryp = "Open"; + break; + case WIFI_AUTH_WEP: + encryp = "WEP"; + break; + case WIFI_AUTH_WPA_PSK: + encryp = "WPA_PSK"; + break; + case WIFI_AUTH_WPA2_PSK: + encryp = "WPA2_PSK"; + break; + case WIFI_AUTH_WPA_WPA2_PSK: + encryp = "WPA_WPA2_PSK"; + break; + case WIFI_AUTH_WPA2_ENTERPRISE: + encryp = "WPA2_ENT"; + break; + default: + encryp = "UNKNOWN"; + break; + } + } + + JsonObject wifi = doc["wifi"].to(); + wifi["client-ip"] = "XXX.XXX.XXX.XXX"; + wifi["mac-addr"] = chipInfo.macStr; + wifi["ch"] = wifi_ch; + wifi["rssi"] = rssi; + wifi["encryp"] = encryp; + + JsonObject ble = doc["ble"].to(); + ble["en"] = (sysProps.appIndex == DSLRBOOTH_INDEX)? false : true; + ble["clients"] = (BTDeviceConnected)? 1 : 0; + ble["ssid"] = BLEDeviceName; + +} + +void handlePOST_Post(AsyncWebServerRequest *request){ + ESP_LOGD(tag, "posts request.."); + if(!getpostSuccess) { request->send(400, "text/plain", "Error"); } + + request->send(200, "text/plain", "Ok"); +} + +void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { + getpostSuccess = false; + Serial.println("post file"); +} + + +// POST Requests +int packets, postTotal; +char postType[24]; +char *postData; +void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + if (!index){ + packets = 0; + int sectors = (total + 1023 + 64) / 1024; + postData = new char[1024 * sectors]; + } + + // Accumulate Data Chunks + memcpy(postData + index, data, len); + packets++; + + if((index + len) >= total){ + ESP_LOGD(tag, "index: %d, len: %d, total: %d", index, len, total); + ESP_LOGD(tag, "packets: %d", packets); + getpostSuccess = false; + + // Check if the request contains the necessary parameters + if (request->hasParam("type", false, false)) { + const char* dataType = request->getParam("type", false, false)->value().c_str(); + ESP_LOGD(tag, "data type: %s", dataType); + + if(!strcmp(dataType, "anim-profile-common") || !strcmp(dataType, "set-countdown")){ + JsonDocument profJson; + DeserializationError error = deserializeJson(profJson, postData, total); + + if(!error){ + animProps.profileIndex = profJson["profile-index"].as(); + + JsonObject countdownJson = profJson["countdown"]; + animProps.frontLight.minDuty = countdownJson["min"].as(); + animProps.frontLight.maxDuty = countdownJson["max"].as(); + animProps.frontLight.holdTime = countdownJson["hold"].as(); + animProps.frontLight.rampTime = countdownJson["ramp"].as(); + + int indexChanged = animProps.profileIndex - countdownJson["profile-index"].as(); + animProps.profileIndex = countdownJson["profile-index"].as(); + if(indexChanged){ + load_animation_profileByIndex(animProps.profileIndex); + } + + // Save to disk/flash + if(dataType[0] == 'a'){ + if(updateJsonDocument(profJson, "/cfg/anim-profile-common.json")){ + Buzzer_Beep(150, 3000); + } + } + else{ + ESP_LOGD(tag, "Post:set-countdown, index: %d, min: %.1f, max: %.1f, hold: %d, ramp: %d", animProps.profileIndex, animProps.frontLight.minDuty, animProps.frontLight.maxDuty, animProps.frontLight.holdTime, animProps.frontLight.rampTime); + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } + + getpostSuccess = true; + //goto done; + } + else if(!strncmp(dataType, "anim-profile", 12)){ + if (strlen(dataType) > 12) { + int profile_index = (dataType[12] - '0' - 1) % 8; // extract index, assuming dataType has enough characters + JsonDocument profJson; + DeserializationError error = deserializeJson(profJson, postData); + + if(!error){ + JsonArray eventsArray = profJson["events"]; + for(int i = 0; i < eventsArray.size(); i++ ){ + animProps.event[i].selfIndex = i; + animProps.event[i].animIndex = eventsArray[i]["anim"].as(); + animProps.event[i].hue = eventsArray[i]["hue"].as(); + animProps.event[i].hueRange = eventsArray[i]["hue-range"].as(); + animProps.event[i].speed = eventsArray[i]["speed"].as(); + animProps.event[i].param1 = eventsArray[i]["param1"].as(); + animProps.event[i].param2 = eventsArray[i]["param2"].as(); + animProps.event[i].check1 = eventsArray[i]["check1"].as(); + animProps.event[i].check2 = eventsArray[i]["check2"].as(); + animProps.event[i].check3 = eventsArray[i]["check3"].as(); + animProps.event[i].check4 = eventsArray[i]["check4"].as(); + animProps.event[i].countDown = 0; + } + + ESP_LOGD(tag, "Extracted/Updated active profile to animProps."); + + // Save to disk/flash + String filePath = "/cfg/anim-profile" + String(profile_index + 1) + ".json"; + if(updateJsonDocument(profJson, filePath.c_str())){ + Buzzer_Beep(150, 3000); + } + + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } + + getpostSuccess = true; + } + } + else if(!strcmp(dataType, "play-anim")){ + JsonDocument animData; + DeserializationError error = deserializeJson(animData, postData); + if(!error){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = animData["index"].as(); + testEvent.animIndex = animData["anim"].as(); + testEvent.hue = animData["hue"].as(); + testEvent.hueRange = animData["hue-range"].as(); + testEvent.speed = animData["speed"].as(); + testEvent.param1 = animData["param1"].as(); + testEvent.param2 = animData["param2"].as(); + testEvent.check1 = animData["check1"].as(); + testEvent.check2 = animData["check2"].as(); + testEvent.check3 = animData["check3"].as(); + testEvent.check4 = animData["check4"].as(); + + if(testEvent.selfIndex != 0){ + testEvent.selfIndex = 1; + } + ESP_LOGI(tag, "Post:play-anim, index: %d, anim: %d, hue: %d, rng: %d, speed: %d, par1: %d, par2: %d, chk1: %d, chk2: %d, chk3: %d, chk4: %d", testEvent.selfIndex, testEvent.animIndex, testEvent.hue, testEvent.hueRange, testEvent.speed, testEvent.param1, testEvent.param2, testEvent.check1, testEvent.check2, testEvent.check3, testEvent.check4); + testEvent.countDown = 0; // set to zero so param1 determines countdown time + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + Buzzer_Beep(150, 3000); + }else{ + ESP_LOGE(tag, "Deserialization error for play-anim"); + } + + getpostSuccess = true; + } + else if(!strcmp(dataType, "wifi")){ + JsonDocument wifiCreds; + DeserializationError error = deserializeJson(wifiCreds, postData, total); + if(!error){ + //read credentials + //const char* new_ssid = wifiCreds["ssid"]; + //const char* new_pass = wifiCreds["pass"]; + + String new_ssid = wifiCreds["ssid"]; + String new_pass = wifiCreds["pass"]; + ESP_LOGV(tag, "SSID: %s PASS: %s", new_ssid, new_pass); + // Save Credentials if different + //if(strcmp(new_ssid, ssid)!=0 || strcmp(new_pass, passPhrase)!=0){ + if(new_ssid != ssid || new_pass != passPhrase){ + Wifi_Save_Credentials(new_ssid, new_pass); + StartDelayedRebooth; + } + }else{ + ESP_LOGE(tag, "Deserialization error for wifi"); + } + + getpostSuccess = true; + } + else if(!strcmp(dataType, "set-pixel")){ + JsonDocument js; + DeserializationError error = deserializeJson(js, postData); + if(!error){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = 1; + testEvent.animIndex = 81; + testEvent.hue = js["hue"].as(); + testEvent.param1 = js["index"].as(); + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + ESP_LOGD(tag, "Post: set-pixel, hue: %d, pixel: %d", testEvent.hue, testEvent.param1); + Buzzer_Beep(150, 3000); + }else{ + ESP_LOGE(tag, "Deserialization error for set-pixel"); + } + + Buzzer_Beep(50, 3000); + getpostSuccess = true; + } + else if(!strcmp(dataType, "clear-strip")){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = 1; + testEvent.animIndex = 80; + ESP_LOGD(tag, "Post: clear-strip"); + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + + getpostSuccess = true; + Buzzer_Beep(50, 3000); + } + else if(!strcmp(dataType, "setup-save")){ + JsonDocument js; + DeserializationError error = deserializeJson(js, postData); + if(!error){ + // If app index is different open app-events.json and update + if(sysProps.appIndex != js["appindex"].as()){ + File file = LittleFS.open("/cfg/app-events.json", "r+"); + if(!file){ + ESP_LOGE(tag, "Failed to open app-events.json"); + file.close(); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if(!error){ + // Update index value + doc["index"] = js["appindex"].as(); + ESP_LOGD(tag, "New App Index = %d", doc["index"].as()); + + if(updateJsonDocument(doc, "/cfg/app-events.json")){ + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for app-events.json"); + } + } + }else{ + ESP_LOGE(tag, "Deserialization error for seteup-save"); + } + + // if any of the values are diiferent then open led-devices.json and update + //if app index is different open app-events.json and update + bool editJson = false; + //if(js["en1"].as() != strip1->size) { editJson = true; } + if(js["count1"].as() != strip1->size) { editJson = true; } + else if(js["shift1"].as() != strip1->shift) { editJson = true; } + else if(js["offset1"].as() != strip1->offset) { editJson = true; } + else if(js["rgb1"].as() != strip1->ledOrder) { editJson = true; } + else if(js["power1"].as() != strip1->powerDiv) { editJson = true; } + + if(editJson){ + File file = LittleFS.open("/cfg/led-devices.json", "r+"); + if(!file){ + ESP_LOGE(tag, "Failed to open led-devices.json"); + file.close(); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if(!error){ + JsonObject jsStrip1 = doc["strip1"].to(); // nested so it doesn't create a copy + + jsStrip1["en"] = true; + jsStrip1["size"] = js["count1"].as(); + jsStrip1["shift"] = js["shift1"].as(); + jsStrip1["offset"] = js["offset1"].as(); + jsStrip1["power-div"] = js["power1"].as(); + jsStrip1["rgb-order"] = js["rgb1"]; + + // Save Json File + if(updateJsonDocument(doc, "/cfg/led-devices.json")){ + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for led-devices.json"); + } + } + + ESP_LOGD(tag, "Post: setup-save"); + getpostSuccess = true; + Buzzer_Beep(150, 3000); + } + else if(!strcmp(dataType, "restart")){ + StartDelayedRebooth; + + ESP_LOGD(tag, "Post: restart"); + getpostSuccess = true; + Buzzer_Beep(150, 3000); + } + } + + // Delete the allocated buffer (owned by the request->_tempObject) + delete[] postData; + } +} + +bool isInternetConnected() +{ + if (WiFi.status() == WL_CONNECTED) { // check if WiFi is connected + WiFiClient client; + const int httpPort = 80; + if (client.connect("www.google.com", httpPort)) { // try to connect to Google + client.stop(); + return true; // Internet access available + } + } + return false; // Internet access not available +} + +void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + if (!index && request->hasParam("dir-path", true, false)) { + AsyncWebParameter* p = request->getParam("dir-path",true, false); + String path = p->value() + "/" + filename; + ESP_LOGD(tag, "upload path: %s", path.c_str()); + + request->_tempFile= LittleFS.open(path, "w"); + //fs::File file = LittleFS.open(path, "w"); + if (!request->_tempFile) { + ESP_LOGE(tag, "Failed to create file!"); + return; + } + + }else{ + ESP_LOGE(tag, "dir-path Param not found.."); + } + + if(len && request->_tempFile){ + request->_tempFile.write(data, len); + } + + if(final){ + request->_tempFile.close(); + request->redirect("/files"); + ESP_LOGD(tag, "UploadEnd: %s, %u B", filename.c_str(), index+len); + } +} + +void handleGET_DSLRBooth(AsyncWebServerRequest *request) +{ + static int lastCountdown = 1000; + request->send(200, "text/plain"); // send this right away to avoid comm delays + + if(request->args() >= 1){ + int x = 0; + const char* event_type = request->arg( x ).c_str(); + + if(strcmp("countdown", event_type) == 0){ + int p = request->arg(1).toInt(); + if(p > 0 && p <= 100){ + animStatus.countStatus = p; + } + + // in case "coundown_start" was missed + if(animStatus.EventsIndex != ANIM_WHITEFILL_INDEX){ + animProps.event[0].countDown = lastCountdown; + animProps.event[0].type = EV_NORMAL; + PostNewEvent(animProps.event[0]); + } + } + else if(strcmp("countdown_start", event_type) == 0){ // index=0 + int count = request->arg(1).toInt(); // retrieve countdown value + //Log.traceln(" countdown = %d", count); + if(count < 1){count = 1;} + count *= 1000; + lastCountdown = count - 500; + animProps.event[0].countDown = lastCountdown; + animProps.event[0].type = EV_NORMAL; + PostNewEvent(animProps.event[0]); + } + else if(strcmp("sharing_screen", event_type) == 0){ + animProps.event[2].type = EV_NORMAL; + PostNewEvent(animProps.event[2]); + } + else if(strcmp("session_end", event_type) == 0){ //index=1 + animProps.event[1].type = EV_NORMAL; + PostNewEvent(animProps.event[1]); + } + else{ + // else if(strcmp_P("session_start", event_type) == 0){ + // TODO determine if ramp down is active depending on session type + + /* + const char* param1 = request->arg(1).c_str(); + if(strcmp("PrintOnly", param1)){ + animStatus.Animation = ANIM_START_PRINTONLY; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_print..\n\r"); + }else if(strcmp("PrintAndGIF", param1)){ + animStatus.Animation = ANIM_START_PRINTGIF; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_prnt_gif..\n\r"); + }else if(strcmp("OnlyGIF", param1)){ + animStatus.Animation = ANIM_START_GIFONLY; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_gif..\n\r"); + }else if(strcmp("Boomerang", param1)){ + animStatus.Animation = ANIM_START_BOOM; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_boom..\n\r"); + }else if(strcmp("Video", param1)){ + animStatus.Animation = ANIM_START_VIDEO; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_vid..\n\r"); + } + */ + } + } +} + +bool isFirmware = true; +bool updateSuccessful = false; +void handlePOST_Update(AsyncWebServerRequest *request) +{ + bool hasError = Update.hasError(); + String responseContent = hasError ? "failed" : "success"; + String responseType = hasError ? "text/plain" : "text/html"; + int responseCode = hasError ? 500 : 200; + + AsyncWebServerResponse *response = request->beginResponse(responseCode, responseType, responseContent); + response->addHeader("status", responseContent); + request->send(response); +} + +// handlePOST_Update Callback function to install the firmware or file system bin files +void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + // Do Once Code + if (!index) { + size_t fileSize = UPDATE_SIZE_UNKNOWN; + // Retrieve file-size parameter + if(request->hasParam("file-size", true, false)) { // make sure param is from POST request or it will not find it + fileSize = request->getParam("file-size", true, false)->value().toInt(); + } + + // register progress callback function + Update.onProgress(updateFirmwareProgress); + updateSuccessful = false; + + //file name checks + if (filename.startsWith("ata_fw")) { + isFirmware = true; + if (!Update.begin(fileSize, U_FLASH, BoardLED2)) { + ESP_LOGD(tag, "Firmware update failed to start"); + return; + } + ESP_LOGD(tag, "Updating Firmware with: %s Size:%d ", filename.c_str(), fileSize); + } + else if (filename.startsWith("ata_fs")) { + isFirmware = false; + if (!Update.begin(fileSize, U_SPIFFS, BoardLED2)) { + ESP_LOGD(tag, "LittleFS update failed to start"); + return; + } + ESP_LOGD(tag, "Updating File System with: %s Size:%d ", filename.c_str(), fileSize); + } + else { + ESP_LOGD(tag, "Unsupported file type"); + return; + } + } + + // Writing... + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + } + } + + // All Done... + if (final) { + vTaskDelay(100); + if(isFirmware) { // firmware update + if (Update.end(true)) { + RebootSystem = true; + updateSuccessful = true; + ESP_LOGD(tag, "firmware update successful: %d", index + len); + } + else { + Update.printError(Serial); + } + } + else{ // file system update + if(Update.end(true)) { + updateSuccessful = true; + ESP_LOGD(tag, "file system update successful!"); + } + else { + Update.printError(Serial); + } + } + } +} + +// Server requested files that aren't template processed +void handleGET_SendFile(AsyncWebServerRequest *request)// try this later "^/(img|favicon).*" +{ + String filePath = request->url(); + const char* ext = getFileExtension(filePath.c_str()); + const char* contentType = getFileType(ext); + + if( contentType == NULL ){ + request->send(404); + return; + } + + if(filePath.c_str()[0] != '/'){ + filePath = '/' + filePath; + } + + if (!strcmp(ext,"html") || !strcmp(ext, "htm") || !strcmp(ext, "css") || !strcmp(ext, "js")) { + if(!filePath.startsWith("/www/")){ + filePath = "/www" + filePath; + } + } + + ESP_LOGD(tag, "Sent: %s", filePath.c_str()); + request->send(LittleFS, filePath, contentType); +} + +String listDir(String directoryList[], int count) +{ + String listedFiles; + + for (int i = 0; i < count; i++) { + // directory html + listedFiles += "Dir: "; + listedFiles += directoryList[i]; + listedFiles += "/-\n"; + + filesDropdownOptions += "\n"; + + dirDropdownOptions += "\n"; + + File dir = LittleFS.open(directoryList[i]); + File file = dir.openNextFile(); + while (file) { + String fileName = file.name(); + if (!file.isDirectory()) { + //Serial.println(" File: " + String(file.name())); + listedFiles += "  "; + listedFiles += fileName; + listedFiles += ""; + listedFiles += convertFileSize(file.size()); + listedFiles += "\n"; + + filesDropdownOptions += "\n"; + } + file = dir.openNextFile(); + } + dir.close(); + } + + return listedFiles; +} + +char* readFile(fs::FS &fs, const char* path) { + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) { + return nullptr; + } + + size_t fileSize = file.size(); + char* fileContent = new char[fileSize + 1]; // +1 for null-terminator + + size_t bytesRead = file.readBytes(fileContent, fileSize); + file.close(); + + fileContent[bytesRead] = '\0'; // Set null-terminating character + + if (bytesRead != fileSize) { + delete[] fileContent; + return nullptr; + } + + return fileContent; +} + +void writeFile(fs::FS &fs, const char * path, const char * message) +{ + File file = fs.open(path, "w"); + if(!file){ + return; + } + file.print(message); + file.close(); +} + +// Send html file with template processing {{VAR}} +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)) +{ + ESP_LOGD(tag, "Sent file: %s", filePath); + File file = LittleFS.open(filePath, "r"); + const char* htmlFile = readFile(LittleFS, filePath); + + //String processedData = varReplace(htmlFile, htmlProcessor); + String processedData = varReplace(htmlFile, callback); + request->send(200, "text/html", processedData); +} + +const char* convertFileSize(const size_t bytes) { + static char fileSizeBuffer[16]; // PreAllocated buffer for the file size + + if (bytes < 1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes); + } else if (bytes < 1024*1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", static_cast(bytes) / 1024.0); + } else if (bytes < 1024*1024*1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", static_cast(bytes) / 1048576.0); + } else { + fileSizeBuffer[0] = '\0'; // Empty string if the file size is too large + } + + return fileSizeBuffer; +} + +// Callback template processor +String htmlProcessor(const String& var) +{ + if(var == "NAVBAR"){ return String("\n"); } + + if(var == "ALLOWED_EXTENSIONS_EDIT"){ return allowedExtensionsForEdit; } + + if(var == "FS_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); } + + if(var == "FS_USED_BYTES"){ return convertFileSize(LittleFS.usedBytes()); } + + if(var == "FS_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "LISTED_FILES"){ + filesDropdownOptions = ""; // clear out + dirDropdownOptions = ""; // clear out + String directories[8]; + int dirCount = 0; + getAllDirectories(directories, dirCount); + + return listDir(directories, dirCount); + } + + if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; } + + if(var == "DIR_LIST"){ return dirDropdownOptions; } + + if(var == "SAVE_PATH_INPUT"){ + if(savePath == "/new.txt"){ + return String(""; + } + else{ + return String(""; + } + } + + if(var == "FIRM_VER"){ return FIRMWARE_VER; } + + return var; +} + +String HomeHtmlProcessor(const String& var) { + if(var == "NAVBAR"){ return String("\n"); } + + if (var == "APP_NAME") { + return sysProps.appName; + } + if (var == "OLED") { + return "No"; + } + if (var == "STRIP1") { + return (strip1) ? "Yes" : "No"; + } + if (var == "STRIP2") { + return (strip2) ? "Yes" : "No"; + } + if (var == "FRONT_LIGHT") { + return (animProps.frontLight.enabled) ? "Yes" : "No"; + } + if (var == "REAR_LIGHT") { + return (animProps.rearLight.enabled) ? "Yes" : "No"; + } + if (var == "FIRMWARE") { + return FIRMWARE_VER; + } + if (var == "BOOTH_T") { + return String(sysProps.t_sensor.temperature) + "F"; + } + if (var == "SETPOINT") { + return String(sysProps.t_sensor.Setpoint1) + "F"; + } + if (var == "FLASH_SIZE") { + return convertFileSize(ESP.getSketchSize()); + } + if (var == "FLASH_FREE") { + return convertFileSize(ESP.getFreeSketchSpace()); + } + if (var == "HEAP_SIZE") { + return convertFileSize(ESP.getHeapSize()); + } + if (var == "HEAP_FREE") { + return convertFileSize(ESP.getFreeHeap()); + } + if (var == "CPU_FREQ") { + return String(ESP.getCpuFreqMHz()) + "Mhz"; + } + if (var == "IP") { + return WiFi.localIP().toString(); + } + if (var == "MAC") { + return chipInfo.macStr; + } + if (var == "SSID") { + return WiFi.SSID(); + } + if (var == "RSSI") { + return String(WiFi.RSSI()); + } + if (var == "WIFI_CH") { + return String(WiFi.channel()); + } + if (var == "ENCRYP") { + return String(WiFi.encryptionType(0)); + } + if (var == "AP_SSID") { + return WiFi.softAPSSID(); + } + if (var == "AP_CLIENTS") { + return String(WiFi.softAPgetStationNum()); + } + if (var == "BLE") { + return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No"; + } + if (var == "BLE_SSID") { + return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : ""; + } + if (var == "BLE_CLIENTS") { + return (BTDeviceConnected) ? "1" : "0"; + } + if (var == "AP_MAC") { + return getSoftAPMacAddress(); + } + + + // Return an empty string if the variable is not recognized + return var; +} + +String getSoftAPMacAddress() { + uint8_t mac[6]; + WiFi.softAPmacAddress(mac); + + String macString = ""; + for (int i = 0; i < 6; i++) { + macString += String(mac[i], HEX); + if (i < 5) macString += ":"; + } + return macString; +} + +// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content +String varReplace(const String& input, String (*callback)(const String&)) { + if (input.isEmpty()) { + return input; + } + + String result; + int startPos = 0; + int start = input.indexOf("{{", startPos); + + while (start != -1) { + result += input.substring(startPos, start); + + int end = input.indexOf("}}", start + 2); + if (end == -1) { + break; + } + + String segment = input.substring(start + 2, end); + int segmentLength = segment.length(); + if (segmentLength <= 32) { + String replacement = callback(segment); + result += replacement; + } else { + result += input.substring(start, end + 2); // Include the original segment if it exceeds the limit + } + + startPos = end + 2; + start = input.indexOf("{{", startPos); + } + + result += input.substring(startPos); + return result; +} + +const char* getFileExtension(const char* filename) { + size_t dotPos = strlen(filename); + while (dotPos > 0 && filename[dotPos - 1] != '.') { + dotPos--; + } + + if (dotPos != 0 && dotPos < strlen(filename) - 1) { + return &filename[dotPos]; + } + + return ""; // Empty string if no extension found +} + +const char* getFileType(const char* ext) { + if (strcmp(ext, "png") == 0) { + return "image/png"; + } else if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) { + return "image/jpeg"; + } else if (strcmp(ext, "gif") == 0) { + return "image/gif"; + } else if (strcmp(ext, "ico") == 0) { + return "image/x-icon"; + } else if (strcmp(ext, "txt") == 0) { + return "text/plain"; + } else if (strcmp(ext, "css") == 0) { + return "text/css"; + } else if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) { + return "text/html"; + } else if (strcmp(ext, "js") == 0) { + return "text/javascript"; + } else if (strcmp(ext, "json") == 0) { + return "application/json"; + } else { + return ""; + } +} + +// Serial print firmware or file system update progress +void updateFirmwareProgress(size_t progress, size_t total) +{ + static int lastProg = -1; + int firmwareProgress = (progress * 100)/ total; + + if(firmwareProgress != lastProg){ + Serial.printf("Progress: %u%%\r", firmwareProgress); + lastProg = firmwareProgress; + //TODO Add buzzer tune while uploading firmware + //Buzzer_Play_Tune(TUNE_DOWNLOADING,1,true,false); + } +} + + diff --git a/temporary/Temp/my_wifi.h b/temporary/Temp/my_wifi.h new file mode 100644 index 0000000..67f0773 --- /dev/null +++ b/temporary/Temp/my_wifi.h @@ -0,0 +1,66 @@ +#ifndef _MY_WIFI_H +#define _MY_WIFI_H + +#include +#include +#include + +extern int RebootSystem; + +void Wifi_Start_WebServer(void); +void Init_Wifi_Task(void); +void Wifi_Start_MDNS(void); +void Wifi_Task(void *parameters); +String getSoftAPMacAddress(void); +//void onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info); +void onWiFiEvent(WiFiEvent_t event); +void Wifi_Request_Task(void *parameters); +void Setup_WebServer_Handlers(AsyncWebServer* serv); + +void Wifi_Read_Credentials(const JsonObject &credJson); +void Wifi_Save_Credentials(String newSSID, String newPass); +bool isInternetConnected(void); +void Wifi_Client_Loop(void); + +// Handlers +void handlePOST_RegStick(AsyncWebServerRequest *request); + +void handleGET_DSLRBooth(AsyncWebServerRequest *request); +void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); +void handlePOST_Update(AsyncWebServerRequest *request); +void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); +void updateFirmwareProgress(size_t progress, size_t total); + +void handleGET_SendFile(AsyncWebServerRequest *request); +void onFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + +void handleGET_Get(AsyncWebServerRequest *request); +void handlePOST_Post(AsyncWebServerRequest *request); +void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final); +void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total); + + +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)); +String htmlProcessor(const String& var); +String HomeHtmlProcessor(const String& var); +//const char* htmlProcessor(const char* var); +String listDir(fs::FS &fs, const char *dirname, bool isSubdirectory); + +const char* getFileExtension(const char* filename); +const char* getFileType(const char* ext); +const char* convertFileSize(const size_t bytes); +char* readFile(fs::FS &fs, const char* path); +void writeFile(fs::FS &fs, const char * path, const char * message); + +String varReplace(const String& input, String (*callback)(const String&)); +//const char* varReplace(const char* input, const char* (*callback)(const char*)); + +void CreateSysSummmaryPacket(JsonDocument &doc); + + + + + + + +#endif \ No newline at end of file diff --git a/temporary/Temp/otaupdate.cpp b/temporary/Temp/otaupdate.cpp new file mode 100644 index 0000000..e18a99c --- /dev/null +++ b/temporary/Temp/otaupdate.cpp @@ -0,0 +1,96 @@ +#include "otaupdate.h" +#include "LittleFS.h" +#include +#include +#include "githubCert.h" +#include + + +String binFile = "manifest.json"; +String rootUrl = "https://raw.githubusercontent.com/ataindustries/atacontrolboardV1/main/"; +String github_token = "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"; + +const float FirmwareVer = 1.0; + +JsonDocument doc; + +int otaupdate_check(void) +{/* + String payload; + int httpCode; + WiFiClientSecure * client = new WiFiClientSecure; + HTTPClient https; + + String manifest_url = rootUrl + binFile + "?token=" + github_token; + https.begin(manifest_url); + httpCode = https.GET(); + + if (httpCode == HTTP_CODE_OK) // if version received + { + String payload = https.getString(); + JsonDocument doc; + deserializeJson(doc, payload); + + float firmver = doc["firmver"].as(); + const char* firmfile = doc["firmfile"]; + const char* fsfile = doc["fsfile"]; + + Serial.printf("From manifest: %.1f, %s, $s\n\r", firmver, firmfile, fsfile); + + if(firmver > FirmwareVer){ + Serial.println("New firmware available!"); + return 1; + }else{ + Serial.println("firmware is upto date!"); + return 0; + } + } + */ + return 0; +} + +void otaupdate_perform(void) +{ + + // setup events + ota_events(); + + + // Pause unnecessary tasks + + //check for update +} + +void ota_events(void) +{ + /* + fota->setUpdateEndCb( [](int partition) + { + //Serial.printf("Update could not finish with %s partition\n", partition==U_SPIFFS ? "spiffs" : "firmware" ); + }); + + fota->setProgressCb( [](size_t progress, size_t size) + { + //if( progress == size || progress == 0 ) Serial.println(); + //Serial.print("."); + }); + + fota->setUpdateCheckFailCb( [](int partition, int error_code) + { + //Serial.printf("Update could validate %s partition (error %d)\n", partition==U_SPIFFS ? "spiffs" : "firmware", error_code ); + // error codes: + // -1 : partition not found + // -2 : validation (signature check) failed + }); + + fota->setUpdateFinishedCb( [](int partition, bool restart_after) + { + //Serial.printf("Update could not begin with %s partition\n", partition==U_SPIFFS ? "spiffs" : "firmware" ); + + if( restart_after ) { + ESP.restart(); + } + }); + */ +} + diff --git a/temporary/Temp/otaupdate.h b/temporary/Temp/otaupdate.h new file mode 100644 index 0000000..0667dea --- /dev/null +++ b/temporary/Temp/otaupdate.h @@ -0,0 +1,14 @@ +#ifndef _OTAUPDATE_H +#define _OTAUPDATE_H + +#include + + +int otaupdate_check(float myVer); + +void otaupdate_perform(String filePath); + +void ota_events(void); + + +#endif \ No newline at end of file diff --git a/temporary/Temp/rf_transceiver.cpp b/temporary/Temp/rf_transceiver.cpp new file mode 100644 index 0000000..1ff5e67 --- /dev/null +++ b/temporary/Temp/rf_transceiver.cpp @@ -0,0 +1,44 @@ + +#include "rf_transceiver.h" + +static const char* tag = "trx433"; +TaskHandle_t RF_Task_Handle; + +const int txChannel = RMT_CHANNEL_0; +const int rxChannel = RMT_CHANNEL_1; + +void Init_RF_Receiver(void) +{ + /* + // Configure RMT receiver + rmt_config_t rxConfig; + rxConfig.channel = (rmt_channel_t)rxChannel; + rxConfig.gpio_num = static_cast(RMT_RX_PIN); + // Set other reception settings as needed + rmt_config(&rxConfig); + rmt_driver_install(rxConfig.channel, 1000, 0); + + xTaskCreatePinnedToCore(RF_Control_Task, "RF_Task", 8000, NULL, 1, &RF_Task_Handle, 0); + */ +} + +void Init_RF_Transmitter(void) +{ + /* + rmt_config_t txConfig; + txConfig.channel = (rmt_channel_t)txChannel; + txConfig.gpio_num = static_cast(RMT_TX_PIN); + // Set other transmission settings as needed + rmt_config(&txConfig); + rmt_driver_install(txConfig.channel, 0, 0); + */ +} + +void RF_Control_Task(void *parameters) +{ + for(;;){ + vTaskDelay(100); + } +} + + diff --git a/temporary/Temp/rf_transceiver.h b/temporary/Temp/rf_transceiver.h new file mode 100644 index 0000000..091368f --- /dev/null +++ b/temporary/Temp/rf_transceiver.h @@ -0,0 +1,23 @@ +#ifndef _RF_TRANSCEIVER_H +#define _RF_TRANSCEIVER_H + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" + +#include +#include +#include "my_board.h" + +#define RMT_RX_PIN RX433_Pin +#define RMT_TX_PIN TX433_Pin +#define RTM_RX_CHANNEL 0 +#define RTM_TX_CHANNEL 1 + +void Init_RF_Receiver(void); + +void Init_RF_Transmitter(void); + +void RF_Control_Task(void *parameters); + +#endif \ No newline at end of file diff --git a/temporary/appcontrol old.html b/temporary/appcontrol old.html new file mode 100644 index 0000000..3033a2a --- /dev/null +++ b/temporary/appcontrol old.html @@ -0,0 +1,623 @@ +{{NAVBAR}} + + + + App Control Configuration + + + + + + +

App Control Configuration

+ + +
+ Saved Animation Profiles + + + + +
+ + +    + + +     + +
+
+ + +
+ + +
+ Countdown Animation ( White Fill ) + + + + + + + + + + + + + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+ +
+ +
+ Event 1 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 2 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 3 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 4 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 5 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 6 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 7 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event 8 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ + + + diff --git a/temporary/bak/cfg/!instructions.txt b/temporary/bak/cfg/!instructions.txt new file mode 100644 index 0000000..1e97b38 --- /dev/null +++ b/temporary/bak/cfg/!instructions.txt @@ -0,0 +1,13 @@ + +Choose Control App: + 1) Open app-events.json + 2) Change index to corresponding app + a. 0=DSLRBooth, 1=...., 2=..... + +Front Constant Light: ( to enable the light) + 1) Open led-devices.json + 2) Change "front-light", "en" to true + +Rear Constant Light: + 1) Open led-devices.json + 2) Change "rear-light", "en" to true \ No newline at end of file diff --git a/temporary/bak/cfg/anim-list.json b/temporary/bak/cfg/anim-list.json new file mode 100644 index 0000000..51dbbc4 --- /dev/null +++ b/temporary/bak/cfg/anim-list.json @@ -0,0 +1,105 @@ +{ + "whitefills":[ + { + "name":"Bottom/Up Fill", + "speed":"Speed: ", + "hue-range":"", + "param1":"Time: ", + "param2":"Const Light Delay: ", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + } + ], + "animations":[ + { + "name":"Rainbow", + "speed":"Speed: ", + "hue-range":"", + "param1":"", + "param2":"", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Hue Spectrum", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"", + "param2":"", + "check1":"Dir Rev ", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Color Pulse Cycle", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"", + "param2":"Colors: ", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Comets", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Random Decay", + "check3":"Shorter Tail ", + "check4":"50% Lum" + }, + { + "name":"Dashes", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Snakes", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Sectors", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Fire", + "speed":"Speed: ", + "hue-range":"Run-time per: ", + "param1":"Cooling: ", + "param2":"Sparking: ", + "check1":"Cycle Colors ", + "check2":"", + "check3":"", + "check4":"50% Lum" + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/anim-profiles.json b/temporary/bak/cfg/anim-profiles.json new file mode 100644 index 0000000..e50c0e4 --- /dev/null +++ b/temporary/bak/cfg/anim-profiles.json @@ -0,0 +1,1203 @@ +{ + "countdown": { + "min": 0, + "max": 98, + "hold": 1000, + "ramp": 500 + }, + "profile-index": 3, + "profiles": [ + { + "name": "Profile 1", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 2", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 3", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 4", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 75, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 1, + "hue": 0, + "hue-range": 340, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 3, + "hue": 0, + "hue-range": 340, + "speed": 75, + "param1": 30, + "param2": 60, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 40, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 5", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 5", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Wedding 7", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Wedding 8", + "events": [ + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/anim-settings.json b/temporary/bak/cfg/anim-settings.json new file mode 100644 index 0000000..3889b0e --- /dev/null +++ b/temporary/bak/cfg/anim-settings.json @@ -0,0 +1,97 @@ +{ + "twinkle":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Strobe":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Solid":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "pwr": 255 + }, + "Fade":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "HueSwirl":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Meteors":{ + "col1": "#000000", + "col2": "#000000", + "range": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Dashes":{ + "col1": "#000000", + "col2": "#000000", + "col3": "#000000", + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "DashesRange":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Fire":{ + "col1": "#000000", + "cool": 25, + "spark": 25, + "speed": 25, + "pwr": 255 + }, + "Rain":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Stacking":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Snake":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Theater":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "step": 20, + "pwr": 255 + } +} \ No newline at end of file diff --git a/temporary/bak/cfg/app-events.json b/temporary/bak/cfg/app-events.json new file mode 100644 index 0000000..fa7b393 --- /dev/null +++ b/temporary/bak/cfg/app-events.json @@ -0,0 +1,90 @@ +{ + "index": 0, + "apps":[ + { + "name": "DSLR-Booth Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + { + "name": "FotoFliqs Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop", + "", + "", + "", + "" + ] + }, + { + "name": "Touchpix Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop", + "", + "", + "", + "" + ] + }, + { + "name": "FotoZap Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + { + "name": "Twineit Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/ble.json b/temporary/bak/cfg/ble.json new file mode 100644 index 0000000..8cd73ca --- /dev/null +++ b/temporary/bak/cfg/ble.json @@ -0,0 +1,9 @@ +{ + "en": "true", + "core": 1, + "device-name": "ATA_COMM", + "key": "123456", + "service-uuid": "6E400001-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-rx": "6E400002-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-tx": "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" +} \ No newline at end of file diff --git a/temporary/bak/cfg/buzzer.json b/temporary/bak/cfg/buzzer.json new file mode 100644 index 0000000..fa2cffd --- /dev/null +++ b/temporary/bak/cfg/buzzer.json @@ -0,0 +1,73 @@ +{ + "en":true, + "boot":{ + "cycles": 1, + "pause": 0, + "tune": "Boot:d=16,o=5,b=112:f,g,a,b,b#" + }, + "restart":{ + "cycles": 1, + "pause": 0, + "tune": "Restart:d=16,o=5,b=112:f,g,a,b,b#" + }, + "wifi-conn":{ + "cycles": 1, + "pause": 0, + "tune": "WifiConn:d=16,o=5,b=112:f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6,f,a#,c6" + }, + "wifi-disc":{ + "cycles": 1, + "pause": 0, + "tune": "WifiDisc:d=16,o=5,b=112:f,g,a,b,b#" + }, + "ble-conn":{ + "cycles": 1, + "pause":0, + "tune": "BleConn:d=16,o=5,b=112:f,g,a,b,b#" + }, + "ble-disc":{ + "cycles": 1, + "pause": 0, + "tune": "BleDisc:d=16,o=5,b=112:f,g,a,b,b#" + }, + "click":{ + "cycles": 1, + "pause": 0, + "tune": "Click:d=16,o=5,b=112:f,g,a,b,b#" + }, + "error":{ + "cycles": 1, + "pause": 0, + "tune": "Error:d=16,o=5,b=112:32p,f,g,a,b,b#" + }, + "success":{ + "cycles": 1, + "pause": 0, + "tune": "Success:d=16,o=5,b=112:f,g,a,b,b#" + }, + "download":{ + "cycles": 1, + "pause": 0, + "tune": "Download:d=16,o=5,b=112:f,g,a,b,b#" + }, + "waiting":{ + "cycles": 1, + "pause": 0, + "tune": "Waiting:d=16,o=5,b=112:b" + }, + "beep":{ + "cycles": 1, + "pause": 0, + "tune": "Beep:d=16,o=5,b=112:b" + }, + "test":{ + "cycles": 1, + "pause": 0, + "tune": "Test:d=16,o=5,b=112:f,g,a,b,b#" + }, + "ack":{ + "cycles": 1, + "pause": 0, + "tune": "Ack:d=16,o=5,b=112:b,b#" + } +} \ No newline at end of file diff --git a/temporary/bak/cfg/firmware.json b/temporary/bak/cfg/firmware.json new file mode 100644 index 0000000..d691b5d --- /dev/null +++ b/temporary/bak/cfg/firmware.json @@ -0,0 +1,8 @@ +{ + "firm-index": 0, + "firm-locations":[ + "http://mylocation1", + "http://mylocation2", + "http://mylocation3" + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/led-devices.json b/temporary/bak/cfg/led-devices.json new file mode 100644 index 0000000..2935910 --- /dev/null +++ b/temporary/bak/cfg/led-devices.json @@ -0,0 +1,42 @@ +{ + "strip1": { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":0, + "offset": 0, + "power-div": 0, + "pin": 3, + "i2s-ch": 0, + "core": 0 + }, + "strip2": { + "en": false, + "size": 20, + "chip": "SK6812", + "rgb-order": "grb", + "shift":0, + "offset": 0, + "power-div": 0, + "pin": 46, + "i2s-ch": 1, + "core": 0 + }, + "front-light": { + "en": true, + "relay": 0, + "core": 1, + "style": 0, + "delay": 0.75 + }, + "rear-light": { + "en": true, + "relay": 1, + "button": 2, + "min": 0, + "max": 100, + "ramp":3000, + "steps":8 + } +} \ No newline at end of file diff --git a/temporary/bak/cfg/luma-stiks.json b/temporary/bak/cfg/luma-stiks.json new file mode 100644 index 0000000..724076b --- /dev/null +++ b/temporary/bak/cfg/luma-stiks.json @@ -0,0 +1,14 @@ +{ + "stiks":[ + { + "en": true, + "ssid": "lumastikXX", + "mac": 5 + }, + { + "en": true, + "ssid": "lumastikXX", + "mac": 6 + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/relays.json b/temporary/bak/cfg/relays.json new file mode 100644 index 0000000..1ecb9fa --- /dev/null +++ b/temporary/bak/cfg/relays.json @@ -0,0 +1,41 @@ +{ +"relays": + [ + { + "pin": 45, + "freq": 500, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "pin": 48, + "freq": 500, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "pin": 47, + "freq": 500, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "pin": 21, + "freq": 500, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/system.json b/temporary/bak/cfg/system.json new file mode 100644 index 0000000..429a075 --- /dev/null +++ b/temporary/bak/cfg/system.json @@ -0,0 +1,37 @@ +{ + "buttons": [ + { + "en": true, + "pin": 8 + }, + { + "en": true, + "pin": 19 + }, + { + "en": true, + "pin": 0 + } + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "fan": { + "en": true, + "relay": 3 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85, + "fan-pwr1": 50, + "sp2": 90, + "fan-pwr2": 100, + "hyst": 1 + }, + "adc": { + "ain1_factor": 1.0 + } +} diff --git a/temporary/bak/cfg/touch-pins.json b/temporary/bak/cfg/touch-pins.json new file mode 100644 index 0000000..d556439 --- /dev/null +++ b/temporary/bak/cfg/touch-pins.json @@ -0,0 +1,30 @@ +{ + "touchpins": + [ + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + } + ] +} \ No newline at end of file diff --git a/temporary/bak/cfg/trx433.json b/temporary/bak/cfg/trx433.json new file mode 100644 index 0000000..6f9cac0 --- /dev/null +++ b/temporary/bak/cfg/trx433.json @@ -0,0 +1,10 @@ +{ +"rx433": { + "en": "true", + "pin": 38 + }, + "tx433": { + "en": "true", + "pin": 16 + } +} \ No newline at end of file diff --git a/temporary/bak/cfg/wifi.json b/temporary/bak/cfg/wifi.json new file mode 100644 index 0000000..550020c --- /dev/null +++ b/temporary/bak/cfg/wifi.json @@ -0,0 +1,26 @@ +{ + "wifi": + { + "en": true, + "mdns-name": "atadev", + "host-name": "ATADeviceXX" + }, + "cred1": + { + "ssid": "DPWifi", + "pass": "dave3.14159" + }, + "cred2": + { + "ssid": "Default", + "pass": "password" + }, + "ap": + { + "ssid": "ATA_AP", + "pass": "12345678", + "ip": [192,168,10.1], + "gateway": [192,168,10,200], + "subnet": [255,255,255,0] + } +} \ No newline at end of file diff --git a/temporary/bak/www/edit.html b/temporary/bak/www/edit.html new file mode 100644 index 0000000..af21eb6 --- /dev/null +++ b/temporary/bak/www/edit.html @@ -0,0 +1,144 @@ +{{NAVBAR}} + + + + Edit file + + + + + +

Edit file

+
+ Editing file: {{SAVE_PATH_INPUT}} +
+
+
+ +
+
+ {{SAVE_PATH_INPUT}} + + + + +
+
+
+ + + + + + \ No newline at end of file diff --git a/temporary/bak/www/edit2.html b/temporary/bak/www/edit2.html new file mode 100644 index 0000000..a4edd4a --- /dev/null +++ b/temporary/bak/www/edit2.html @@ -0,0 +1,170 @@ +{{NAVBAR}} + + + + Edit file + + + + + +

Edit file

+ +
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/temporary/bak/www/event-box.js b/temporary/bak/www/event-box.js new file mode 100644 index 0000000..f587666 --- /dev/null +++ b/temporary/bak/www/event-box.js @@ -0,0 +1,420 @@ +class EventBox extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + +
+
+ Event0
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+
+ `; + this.Title = this.shadowRoot.querySelector('#center-text'); + this.hueSelect = this.shadowRoot.querySelector('#hue-selector'); + this.AnimationList = this.shadowRoot.getElementById('animation-list'); + this.Speed = this.shadowRoot.getElementById('speed'); + this.HueRange = this.shadowRoot.querySelector('#huerange'); + this.Param1 = this.shadowRoot.querySelector('#param1'); + this.Param2 = this.shadowRoot.querySelector('#param2'); + this.Check1 = this.shadowRoot.querySelector('#check1'); + this.Check2 = this.shadowRoot.querySelector('#check2'); + this.Check3 = this.shadowRoot.querySelector('#check3'); + + this.AnimationListLabel = this.shadowRoot.querySelector('#list-label'); + this.SpeedLabel = this.shadowRoot.querySelector('#speed-label'); + this.HueRangeLabel = this.shadowRoot.querySelector('#huerange-label'); + this.Param1Label = this.shadowRoot.getElementById('param1-label'); + this.Param2Label = this.shadowRoot.getElementById('param2-label'); + this.Check1Label = this.shadowRoot.getElementById('check1-label'); + this.Check2Label = this.shadowRoot.getElementById('check2-label'); + this.Check3Label = this.shadowRoot.getElementById('check3-label'); + + this.AnimationPropsJson; + + this.SpeedCaption = ""; + this.HueRangeCaption = ""; + this.Param1Caption = ""; + this.Param2Caption = ""; + this.Check1Caption = ""; + this.Check2Caption = ""; + this.Check3Caption = ""; + + this.setSpeedCaption(`Speed: `); + this.Speed.min = 0; + this.Speed.max = 100; + + this.setHueRangeCaption(`Hue Range: `); + this.HueRange.min = 0; + this.HueRange.max = 360; + + this.setParam1Caption(`Param1: `); + this.Param1.min = 0; + this.Param1.max = 100; + + this.setParam2Caption(`Param2: `); + this.Param2.min = 0; + this.Param2.max = 100; + + this.setCheck1Caption(`Check1`); + this.setCheck2Caption(`Check2`); + this.setCheck2Caption(`Check3`); + + this.Index = 0; + + this.Speed.addEventListener('input', () => { this.updateSpeedLabel(); }); + this.HueRange.addEventListener('input', () => { this.updateHueRangeLabel(); }); + this.Param1.addEventListener('input', () => { this.updateParam1Label(); }); + this.Param2.addEventListener('input', () => { this.updateParam2Label(); }); + + this.TryButton = this.shadowRoot.querySelector('#try-button'); + this.TryButton.addEventListener('click', () => { + this.handleTryButtonClick(); + }); + + this.AnimationList.addEventListener('change', this.handleAnimationListChange.bind(this)); + } + + setIndex(i){ this.Index = i; } + getIndex(){ return this.Index; } + updateSpeedLabel(){ + if(!this.Speed.hidden){ + this.SpeedLabel.innerHTML = this.SpeedCaption + this.getSpeedValue(); + } + } + updateHueRangeLabel(){ + if(!this.HueRange.hidden){ + this.HueRangeLabel.innerHTML = this.HueRangeCaption + this.getHueRangeValue(); + } + } + updateParam1Label(){ + if(!this.Param1.hidden){ + this.Param1Label.innerHTML = this.Param1Caption + this.getParam1Value(); + } + } + updateParam2Label(){ + if(!this.Param2.hidden){ + this.Param2Label.innerHTML = this.Param2Caption + this.getParam2Value(); + } + } + setTitle(text){ + this.Title.textContent = text; + } + addOptionToList(value, text){ + const optionElement = document.createElement('option'); + optionElement.value = value; + optionElement.textContent = text; + this.AnimationList.appendChild(optionElement); + } + setHidden(hidden){ this.hidden = hidden; } + getHidden(){ return this.hidden; } + setHueValue(value){ this.hueSelect.setHue(value); } + getHueValue(){ return this.hueSelect.getSelectedHue(); } + setSpeedValue(value){ + this.Speed.value = value; + this.updateSpeedLabel(); + } + setSpeedCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.SpeedCaption = caption; + this.SpeedLabel.innerHTML = caption; + this.Speed.hidden = hid; + this.SpeedLabel.hidden = hid; + } + getSpeedValue(){ return this.Speed.value; } + setHueRangeValue(value){ + this.HueRange.value = value; + this.updateHueRangeLabel() + } + setHueRangeCaption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.HueRangeCaption = caption; + this.HueRangeLabel.innerHTML = caption; + this.HueRange.hidden = hid; + this.HueRangeLabel.hidden = hid; + } + getHueRangeValue(){ return this.HueRange.value; } + setParam1Value(value){ + this.Param1.value = value; + this.updateParam1Label() + } + setParam1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param1Caption = caption; + this.Param1Label.innerHTML = caption; + this.Param1.hidden = hid; + this.Param1Label.hidden = hid; + } + getParam1Value(){ return this.Param1.value; } + setParam2Value(value){ + this.Param2.value = value; + this.updateParam2Label() + } + setParam2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Param2Caption = caption; + this.Param2Label.innerHTML = caption; + this.Param2.hidden = hid; + this.Param2Label.hidden = hid; + } + getParam2Value(){ return this.Param2.value; } + + setCheck1Value(value){ this.Check1.checked = value; } + setCheck1Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check1Label.innerHTML = caption; + this.Check1.hidden = hid; + this.Check1Label.hidden = hid; + } + getCheck1Value(){ return this.Check1.checked; } + + setCheck2Value(value){ this.Check2.checked = value; } + setCheck2Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check2Label.innerHTML = caption; + this.Check2.hidden = hid; + this.Check2Label.hidden = hid; + } + getCheck2Value(){ return this.Check2.checked; } + + setCheck3Value(value){ this.Check3.checked = value; } + setCheck3Caption(caption){ + let hid = false; + if(caption.trim() === ""){ + hid = true; + } + this.Check3Label.innerHTML = caption; + this.Check3.hidden = hid; + this.Check3Label.hidden = hid; + } + getCheck3Value(){ return this.Check3.checked; } + + setAnimationIndex(index){ + this.AnimationList.selectedIndex = index; + const changeEvent = new Event('change'); + this.AnimationList.dispatchEvent(changeEvent); + } + getAnimationIndex(){ + return this.AnimationList.selectedIndex; + } + + handleTryButtonClick() { + const eventIndexValue = this.getIndex(); + const animIndexValue = this.getAnimationIndex(); + const hueValue = this.getHueValue(); + const speedValue = this.getSpeedValue(); + const colorRangeValue = this.getHueRangeValue(); + const param1Value = this.getParam1Value(); + const param2Value = this.getParam2Value(); + const check1Value = this.getCheck1Value(); + const check2Value = this.getCheck2Value(); + const check3Value = this.getCheck3Value(); + + this.dispatchEvent(new CustomEvent('tryClick', { + detail: { + eventIndex: eventIndexValue, + animIndex: animIndexValue, + hue: hueValue, + speed: speedValue, + colorRange: colorRangeValue, + param1: param1Value, + param2: param2Value, + check1: check1Value, + check2: check2Value, + check3: check2Value + } + })); + } + + setAnimationCaptions(propsJson){ + this.AnimationPropsJson = propsJson; + + //add options to list + let x = 0; + this.AnimationPropsJson.forEach(props => { + this.addOptionToList(x, props.name); + x++; + }); + + } + + handleAnimationListChange(){ + const selectedIndex = this.getAnimationIndex(); + const selectedProps = this.AnimationPropsJson[selectedIndex]; + + this.updateControlProps(selectedProps); + } + + updateControlProps(props){ + this.setSpeedCaption(props.speed); + let s = props['hue-range']; + this.setHueRangeCaption(s || ""); + this.setParam1Caption(props.param1 || ""); + this.setParam2Caption(props.param2 || ""); + this.setCheck1Caption(props.check1 || ""); + this.setCheck2Caption(props.check2 || ""); + this.setCheck3Caption(props.check3 || ""); + + this.updateSpeedLabel(); + this.updateHueRangeLabel(); + this.updateParam1Label(); + this.updateParam2Label(); + } + +} + +customElements.define('event-box', EventBox); + \ No newline at end of file diff --git a/temporary/bak/www/failed.html b/temporary/bak/www/failed.html new file mode 100644 index 0000000..a092d0c --- /dev/null +++ b/temporary/bak/www/failed.html @@ -0,0 +1,24 @@ +{{NAVBAR}} + + + + Update Failed + + + + + +
+

The update has failed.

+
+ +
+ + \ No newline at end of file diff --git a/temporary/bak/www/files.html b/temporary/bak/www/files.html new file mode 100644 index 0000000..b0f7f69 --- /dev/null +++ b/temporary/bak/www/files.html @@ -0,0 +1,311 @@ +{{NAVBAR}} + + + + File Manager + + + + + + + +

File Manager

+ +
+ File list +
+

  Total: {{FS_TOTAL_BYTES}}, Used: {{FS_USED_BYTES}}, Available: {{FS_FREE_BYTES}}

+
+ + {{LISTED_FILES}} +
Listing: Size:
+
+
+ +
+ +
+ File upload +
+
+ + + + + + + + + + + +
+
+    + +
+
+
+ +
+
+
+ +
+ +
+ Edit file +
+
+ + + +
+
+
+
+ +
+ +
+ Delete file +
+
+ + + +
+
+
+
+ +
+ + + +

Firmware/File System Update

+ +
+ Firmware Update (Local) | Firmware Ver: {{FIRM_VER}}
File name fwataVxxx.bin or lfsataVxxx.bin
+
+
+ + + + + +
+ + + +
+ +
+ + +
+ + + +
+
+
+ +
+ + +
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/temporary/bak/www/global-style.css b/temporary/bak/www/global-style.css new file mode 100644 index 0000000..442f988 --- /dev/null +++ b/temporary/bak/www/global-style.css @@ -0,0 +1,74 @@ +body { + /*background-color: #f7f7f7;*/ + font-family: Tahoma, Arial, sans-serif; + font-size: small; + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} +h1 { + margin-top: 0; +} +input{ + cursor:pointer; +} +input[type="number"]{ + width: 100px; +} +#submit { + width:120px; +} +select{ + width:160px; +} +#select-path { + width:250px; +} +#select-dir { + width:90px; +} +#spacer-50 { + height: 50px; +} +#spacer-20 { + height: 20px; +} +#spacer-10 { + height: 10px; +} +table { + /*background-color: #dddddd;*/ + border-collapse: collapse; + width:600px; + margin: 0 auto; + overflow: visible; +} +td, th { + /*border: 1px solid #dddddd;*/ + text-align: left; + padding: 2px; +} +#first_td_th { + width:400px; +} +fieldset { + width:620px; + /*background-color: #f7f7f7;*/ + border-radius: 10px; + border-color: blue; +} +#format-notice { + color: #ff0000; +} +legend { + display: flex; + justify-content: center; + background-color:white; + background-blend-mode: darken; + border-radius: 10px; + padding: 1px 8px 2px 8px; + border-style: solid; + border-width: 1.0; +} \ No newline at end of file diff --git a/temporary/bak/www/home.html b/temporary/bak/www/home.html new file mode 100644 index 0000000..86a0873 --- /dev/null +++ b/temporary/bak/www/home.html @@ -0,0 +1,271 @@ +{{NAVBAR}} + + + + + + + Welcome + + + + +

ATA Booth Summary

+ +
+ Booth Overview +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + +
+
+
+ ... + + ... + + +
+
+
+ +
+ +
+ System Info: +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + +
+
+
+ + + ... + + +
+
+
+ +
+ +
+ Network / WiFi +
+ + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + +
+
+
+ +
+ +
+ Bluetooth LE +
+ + + + + + + +
+ + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/temporary/bak/www/hue-select.js b/temporary/bak/www/hue-select.js new file mode 100644 index 0000000..bcc8132 --- /dev/null +++ b/temporary/bak/www/hue-select.js @@ -0,0 +1,275 @@ + +// Define the HueSelect custom element +class HueSelect extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.shadowRoot.innerHTML = ` + + + `; + + this.currentHue = 0; + this.colorList; + this.hueLabel = this.shadowRoot.getElementById('hue-label'); + + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + /* colorPatch.addEventListener('mouseenter', () => { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = 'block'; + }); */ + + // Add the global click event listener to hide the color picker dropdown + /* + window.addEventListener('click', (event) => { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + if (!event.target.closest('.dropdown')) { + dropdownContent.style.display = 'none'; + } + }); */ + + selectedColorDiv.addEventListener('click', () => { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = dropdownContent.style.display === 'block' ? 'none' : 'block'; + }); + + // Generate the color options and add them to the dropdown + this.createColorOptions(); + this.setHue(0); + } + + generateColors() { + const colors = []; + for (let hue = 0; hue <= 360; hue += 10) { + if (hue == 360) { hue = 359; } + colors.push(hue); + } + + colors.push(-1); + colors.push(-2); + return colors; + } + + createColorOption(hue) { + const colorOption = document.createElement('div'); + colorOption.classList.add('color-option'); + + const colorPatch = document.createElement('span'); + colorPatch.classList.add('color-patch'); + + const colorText = document.createElement('span'); + colorText.classList.add('color-text'); + colorText.textContent = hue; + + const rgbHex = document.createElement('span'); + rgbHex.classList.add('rgb-hex'); + + if (hue === -2) { + colorPatch.style.backgroundColor = 'rgb(0,0,0)'; + rgbHex.innerHTML = '  #000000'; + } else if (hue === -1) { + colorPatch.style.backgroundColor = 'rgb(255,255,255)'; + rgbHex.innerHTML = '  #FFFFFF'; + } else { + colorPatch.style.backgroundColor = `hsl(${hue}, 100%, 50%)`; + const hexColor = this.hslToRgb(hue, 100, 50).toUpperCase(); + rgbHex.innerHTML = `  ${hexColor}`; + } + + colorOption.appendChild(colorPatch); + colorOption.appendChild(colorText); + colorOption.appendChild(rgbHex); + + // Add click event listener to each color option + colorOption.addEventListener('click', () => this.handleColorSelection(hue)); + + return colorOption; + } + + createColorOptions() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + this.colorList = this.generateColors(); + + this.colorList.forEach(hue => { + const colorOption = this.createColorOption(hue); + dropdownContent.appendChild(colorOption); + }); + + // Create elements for white and black colors + //const whiteColorOption = this.createColorOption(-1); + //dropdownContent.appendChild(whiteColorOption); + + //const blackColorOption = this.createColorOption(-2); + //dropdownContent.appendChild(blackColorOption); + } + + // Function to convert HSL to RGB + hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + let r, g, b; + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = (x) => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + + handleColorSelection(hue) { + this.currentHue = hue; + const selectedColorDiv = this.shadowRoot.getElementById('selectedColor'); + const colorPatch = selectedColorDiv.querySelector('.color-patch'); + + // Update the color patch and hue label + const rgb = this.getRGBfromHue(hue); + colorPatch.style.backgroundColor = rgb; + //console.log(colorPatch.style.backgroundColor); + this.setHueLabel(hue); + this.hideDropdown(); + + // Dispatch a 'change' event with the selected hue value + this.dispatchEvent(new CustomEvent('change', { detail: { hue, rgb } })); + } + + // Method to get the hue value of the selected item + getSelectedHue() { + return this.currentHue; + } + + getSelectedRGB() { + const selectedColorText = this.shadowRoot.getElementById('selectedColor').textContent.trim(); + return parseFloat(selectedColorText); + } + + getRGBfromHue(hue){ + if(hue == -1){ + return '#FFFFFF'; + }else if(hue == -2){ + return '#000000'; + }else{ + return this.hslToRgb(hue, 100, 50);; + } + } + // Method to get the RGB value of the selected item + getSelectedRgb() { + const selectedHue = this.getSelectedHue(); + return getRGBfromHue(selectedHue); + } + + setHue(hue) { + // Round the input hue to the nearest 10th + let roundedHue = hue; + if(hue === -1 || hue === -2){ + roundedHue = hue; + }else{ + roundedHue = Math.round(hue / 10) * 10; + if (roundedHue >= 360) { + roundedHue = 359; + } + + if (roundedHue < 0) { + roundedHue = 0; + } + } + + this.currentHue = roundedHue; + + this.colorList.forEach(colorVal => { + if (colorVal === roundedHue) { + this.handleColorSelection(roundedHue); + } + }); + + this.hideDropdown(); + } + + hideDropdown() { + const dropdownContent = this.shadowRoot.querySelector('.dropdown-content'); + dropdownContent.style.display = 'none'; + } + + setHueLabel(value){ + this.hueLabel.textContent = "Hue: " + value; + } + + + } + + + + // Register the custom element + customElements.define('hue-select', HueSelect); + \ No newline at end of file diff --git a/temporary/bak/www/label.html b/temporary/bak/www/label.html new file mode 100644 index 0000000..6593d48 --- /dev/null +++ b/temporary/bak/www/label.html @@ -0,0 +1,56 @@ + + + + + + +Event Container + + + +
+
+ +
+
+ + + + \ No newline at end of file diff --git a/temporary/bak/www/lights.html b/temporary/bak/www/lights.html new file mode 100644 index 0000000..24814d8 --- /dev/null +++ b/temporary/bak/www/lights.html @@ -0,0 +1,452 @@ +{{NAVBAR}} + + + + + + + + + + + + +
+

Lights Configuration

+ +
+ Saved Animation Profiles +
+
+ + +
+
+ + + +
+
+
+
+ +
+
+ Countdown ( Constant Light ) +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ +
+ + + + diff --git a/temporary/bak/www/navbar.html b/temporary/bak/www/navbar.html new file mode 100644 index 0000000..3c23772 --- /dev/null +++ b/temporary/bak/www/navbar.html @@ -0,0 +1,55 @@ + + + + + + +
+ +
+ + \ No newline at end of file diff --git a/temporary/bak/www/ok.html b/temporary/bak/www/ok.html new file mode 100644 index 0000000..a5263dd --- /dev/null +++ b/temporary/bak/www/ok.html @@ -0,0 +1,24 @@ +{{NAVBAR}} + + + + Update Success + + + + + +
+

The update was successful.

+
+ +
+ + \ No newline at end of file diff --git a/temporary/bak/www/setup.html b/temporary/bak/www/setup.html new file mode 100644 index 0000000..2cfaa4e --- /dev/null +++ b/temporary/bak/www/setup.html @@ -0,0 +1,355 @@ +{{NAVBAR}} + + + + Booth Configuration Tools + + + + + + +

System Setup

+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ LED Strip #1 Settings +
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ + +
+ Test Tool + +
+
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ Luma Stiks + +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+ +
+
+ +
+ + + + \ No newline at end of file diff --git a/temporary/bak/www/wifi.html b/temporary/bak/www/wifi.html new file mode 100644 index 0000000..58f9781 --- /dev/null +++ b/temporary/bak/www/wifi.html @@ -0,0 +1,127 @@ + + + + + WiFi Credentials + + + +

Enter WiFi Credentials

+
+ + + + + + Image not found +
+ + + + diff --git a/temporary/bleconfig.html b/temporary/bleconfig.html new file mode 100644 index 0000000..fac456f --- /dev/null +++ b/temporary/bleconfig.html @@ -0,0 +1,16 @@ +{{NAVBAR}} + + + + BLE Config + + + + + + +

App Control - Bluetooth Configuration

+ + \ No newline at end of file diff --git a/temporary/colorPicket.html b/temporary/colorPicket.html new file mode 100644 index 0000000..fe07814 --- /dev/null +++ b/temporary/colorPicket.html @@ -0,0 +1,31 @@ + + + +Color Picker + + + +
+

Countdown Animation

+
+ + + + +
+

Selection Screen

+
+ +
+ + + diff --git a/temporary/eventTest.htm b/temporary/eventTest.htm new file mode 100644 index 0000000..b30e777 --- /dev/null +++ b/temporary/eventTest.htm @@ -0,0 +1,145 @@ + + + + + + +Event Container + + + +
+
+ Event0
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ Mix  + Fwd +
+
+ +
+
+ +
+
+ + diff --git a/temporary/gridstyles.css b/temporary/gridstyles.css new file mode 100644 index 0000000..bf326f6 --- /dev/null +++ b/temporary/gridstyles.css @@ -0,0 +1,6 @@ +.container{ + color:blue; + display: grid; + gap: 1rem; + grid-template-columns: 50% 1fr 1fr; +} \ No newline at end of file diff --git a/temporary/gridtest.html b/temporary/gridtest.html new file mode 100644 index 0000000..872e384 --- /dev/null +++ b/temporary/gridtest.html @@ -0,0 +1,12 @@ + + + CSS Grid + + +
+
Test text 1
+
Test text 1
+
Test text 1
+
Test text 1
+
+ \ No newline at end of file diff --git a/temporary/hue-select-min.js b/temporary/hue-select-min.js new file mode 100644 index 0000000..e25f5aa --- /dev/null +++ b/temporary/hue-select-min.js @@ -0,0 +1 @@ +class HueSelect extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this.shadowRoot.innerHTML='\n\t\t\n\t\t\n\t ',this.currentHue=0,this.colorList,this.hueLabel=this.shadowRoot.getElementById("hue-label");this.shadowRoot.getElementById("selectedColor").querySelector(".color-patch").addEventListener("mouseenter",(()=>{this.shadowRoot.querySelector(".dropdown-content").style.display="block"})),window.addEventListener("click",(t=>{const e=this.shadowRoot.querySelector(".dropdown-content");t.target.closest(".dropdown")||(e.style.display="none")})),this.createColorOptions(),this.setHue(0)}generateColors(){const t=[];for(let e=0;e<=360;e+=10)360==e&&(e=359),t.push(e);return t.push(-1),t.push(-2),t}createColorOption(t){const e=document.createElement("div");e.classList.add("color-option");const o=document.createElement("span");o.classList.add("color-patch");const n=document.createElement("span");n.classList.add("color-text"),n.textContent=t;const s=document.createElement("span");if(s.classList.add("rgb-hex"),-2===t)o.style.backgroundColor="rgb(0,0,0)",s.innerHTML="  #000000";else if(-1===t)o.style.backgroundColor="rgb(255,255,255)",s.innerHTML="  #FFFFFF";else{o.style.backgroundColor=`hsl(${t}, 100%, 50%)`;const e=this.hslToRgb(t,100,50).toUpperCase();s.innerHTML=`  ${e}`}return e.appendChild(o),e.appendChild(n),e.appendChild(s),e.addEventListener("click",(()=>this.handleColorSelection(t))),e}createColorOptions(){const t=this.shadowRoot.querySelector(".dropdown-content");this.colorList=this.generateColors(),this.colorList.forEach((e=>{const o=this.createColorOption(e);t.appendChild(o)}))}hslToRgb(t,e,o){let n,s,l;if(t/=360,o/=100,0===(e/=100))n=s=l=o;else{const r=(t,e,o)=>(o<0&&(o+=1),o>1&&(o-=1),o<1/6?t+6*(e-t)*o:o<.5?e:o<2/3?t+(e-t)*(2/3-o)*6:t),i=o<.5?o*(1+e):o+e-o*e,d=2*o-i;n=r(d,i,t+1/3),s=r(d,i,t),l=r(d,i,t-1/3)}const r=t=>{const e=Math.round(255*t).toString(16);return 1===e.length?"0"+e:e};return`#${r(n)}${r(s)}${r(l)}`}handleColorSelection(t){this.currentHue=t;const e=this.shadowRoot.getElementById("selectedColor").querySelector(".color-patch"),o=this.getRGBfromHue(t);e.style.backgroundColor=o,console.log(e.style.backgroundColor),this.setHueLabel(t),this.hideDropdown(),this.dispatchEvent(new CustomEvent("change",{detail:{hue:t,rgb:o}}))}getSelectedHue(){return this.currentHue}getSelectedRGB(){const t=this.shadowRoot.getElementById("selectedColor").textContent.trim();return parseFloat(t)}getRGBfromHue(t){return-1==t?"#FFFFFF":-2==t?"#000000":this.hslToRgb(t,100,50)}getSelectedRgb(){const t=this.getSelectedHue();return getRGBfromHue(t)}setHue(t){let e=t;-1===t||-2===t?e=t:(e=10*Math.round(t/10),e>=360&&(e=359),e<0&&(e=0)),this.currentHue=e,this.colorList.forEach((t=>{t===e&&this.handleColorSelection(e)})),this.hideDropdown()}hideDropdown(){this.shadowRoot.querySelector(".dropdown-content").style.display="none"}setHueLabel(t){this.hueLabel.textContent="Hue: "+t}}customElements.define("hue-select",HueSelect); \ No newline at end of file diff --git a/temporary/lights-old.html b/temporary/lights-old.html new file mode 100644 index 0000000..62a71d5 --- /dev/null +++ b/temporary/lights-old.html @@ -0,0 +1,755 @@ +{{NAVBAR}} + + + + App Control Configuration + + + + + + +

App Control Configuration

+ +
+ Saved Animation Profiles + + + + +
+
+ + +    + + +     + +
+
+ + +
+ + +
+ Countdown ( Constant Light ) + + + + + + + + + + + + + + +
+
+ +
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+ +
+ Event0 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event1 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event2 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event3 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event4 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event5 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event6 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event7 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event8 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event9 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ + +
+ +
+ Event10 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ +
+ +
+ Event11 + + + + + + + + + +
+
+ +
+
+   +
+
+   +
+
+ +
+
+ +
+ +
+
+ + + + diff --git a/temporary/log.html b/temporary/log.html new file mode 100644 index 0000000..8684117 --- /dev/null +++ b/temporary/log.html @@ -0,0 +1,10 @@ + + + + + System Log + + +

System Log

+ + \ No newline at end of file diff --git a/temporary/neo_colors.h b/temporary/neo_colors.h new file mode 100644 index 0000000..0ffaf31 --- /dev/null +++ b/temporary/neo_colors.h @@ -0,0 +1,78 @@ +#ifndef NEO_COLORS_H +#define NEO_COLORS_H + +#include + +#define USE_CORRECTED_COLORS 0 + +const RgbColor col_black(0, 0, 0); +const RgbColor col_white(255, 255, 255); +const RgbColor col_red(255, 0, 0); +const RgbColor col_green(0, 255, 0); +const RgbColor col_blue(0, 0, 255); + +#if USE_CORRECTED_COLORS == 0 + +const RgbColor col_orange(255, 165, 0); +const RgbColor col_yellow(255, 255, 0); +const RgbColor col_cyan(0, 255, 255); +const RgbColor col_magenta(255, 0, 255); +const RgbColor col_purple(128, 0, 128); +const RgbColor col_pink(255, 192, 203); +const RgbColor col_teal(0, 128, 128); +const RgbColor col_lime(0, 255, 0); +const RgbColor col_indigo(75, 0, 130); +const RgbColor col_maroon(128, 0, 0); +const RgbColor col_navy(0, 0, 128); +const RgbColor col_olive(128, 128, 0); +const RgbColor col_beige(245, 245, 220); +const RgbColor col_brown(165, 42, 42); +const RgbColor col_coral(255, 127, 80); +const RgbColor col_gold(255, 215, 0); +const RgbColor col_gray(128, 128, 128); +const RgbColor col_ivory(255, 255, 240); +const RgbColor col_khaki(240, 230, 140); +const RgbColor col_lavender(230, 230, 250); +const RgbColor col_peach(255, 218, 185); +const RgbColor col_periwinkle(204, 204, 255); +const RgbColor col_salmon(250, 128, 114); +const RgbColor col_sienna(160, 82, 45); +const RgbColor col_silver(192, 192, 192); +const RgbColor col_tan(210, 180, 140); +const RgbColor col_turquoise(64, 224, 208); +const RgbColor col_violet(238, 130, 238); + +#else + +const RgbColor col_orange(255, 128, 0); +const RgbColor col_yellow(255, 255, 0); +const RgbColor col_cyan(0, 255, 255); +const RgbColor col_magenta(255, 0, 255); +const RgbColor col_purple(170, 0, 255); +const RgbColor col_pink(255, 170, 255); +const RgbColor col_teal(0, 128, 128); +const RgbColor col_lime(128, 255, 0); +const RgbColor col_indigo(85, 0, 255); +const RgbColor col_maroon(128, 0, 0); +const RgbColor col_navy(0, 0, 128); +const RgbColor col_olive(128, 128, 0); +const RgbColor col_beige(255, 230, 204); +const RgbColor col_brown(153, 51, 0); +const RgbColor col_coral(255, 102, 102); +const RgbColor col_gold(204, 153, 0); +const RgbColor col_gray(128, 128, 128); +const RgbColor col_ivory(255, 255, 204); +const RgbColor col_khaki(204, 204, 0); +const RgbColor col_lavender(204, 153, 255); +const RgbColor col_peach(255, 204, 153); +const RgbColor col_periwinkle(153, 153, 255); +const RgbColor col_salmon(255, 153, 102); +const RgbColor col_sienna(153, 76, 0); +const RgbColor col_silver(204, 204, 204); +const RgbColor col_tan(204, 153, 102); +const RgbColor col_turquoise(0, 204, 204); +const RgbColor col_violet(204, 0, 255); + +#endif + +#endif \ No newline at end of file diff --git a/temporary/styles.css b/temporary/styles.css new file mode 100644 index 0000000..94eac3f --- /dev/null +++ b/temporary/styles.css @@ -0,0 +1,50 @@ +.blue { + color: blue; +} + +h1{ + color: yellow; +} + +body{ + color: red; +} + +.secTitleFont{ + color: white; + +} +.secTitle{ + min-width: none; + height: 25px; + width: auto; + padding: 2px; + margin: 2px; + display: flex; + justify-content: center; + align-items: center; + /*border: 3px solid black;*/ + color: rgb(56, 132, 255); + background-color: rgb(61, 118, 153); + font-family: Tahoma, Arial, sans-serif; + font-size: smaller; + font-weight: bold; + flex-direction: ; +} + +.secCntrls{ + height: 100px; + width: auto; + margin: 5px; + border: 1px solid rgb(255, 255, 255); + color: rgba(255, 255, 255, 0); + font-family: Tahoma, Arial, sans-serif; + /*display: flex; + justify-content: center; + align-items: center; + position: relative;*/ +} + +.dropList{ + margin-right: 40px; +} \ No newline at end of file diff --git a/temporary/upgrade.html b/temporary/upgrade.html new file mode 100644 index 0000000..7afb28a --- /dev/null +++ b/temporary/upgrade.html @@ -0,0 +1,228 @@ + + + + Firmware Update + + + + + + + + + +

Firmware Update

+ +
+
+ Local Update +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ Web Update +
+ +
+ +
+ +
+ +
+
+ +
+
+ Update Progress +
+ + + +
+
+
+ + + + \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/webSock/AppUpgrade.cpp b/webSock/AppUpgrade.cpp new file mode 100644 index 0000000..d3a2857 --- /dev/null +++ b/webSock/AppUpgrade.cpp @@ -0,0 +1,522 @@ +#include "AppUpgrade.h" +#include "esp_log.h" +#include +#include +#include +#include "global.h" +#include "jsonConstrain.h" + +static const char* TAG = "AppUpdater"; +TaskHandle_t Update_Task_Handle = NULL; + +// Queue handle for firmware update messages +//QueueHandle_t updateMsgQueue = NULL; + + +bool Version::operator<(const Version& other) const { + if (major != other.major) return major < other.major; + if (minor != other.minor) return minor < other.minor; + return patch < other.patch; +} + +String Version::toString() const { + return String(major) + "." + String(minor) + "." + String(patch); +} + +AppUpdater::AppUpdater(fs::FS& fs, const char* currVersion, const char* bucket, const char* manifestName, const char* appBin) + : currentVersion(currVersion), bucketUrl(bucket), manifestName(manifestName), appName(appBin), fileSystem(fs), downloadBuffer(new uint8_t[BUFFER_SIZE]) +{ + ESP_LOGI(TAG, "AppUpdater initialized with version %s", currVersion); +} + +void AppUpdater::setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)) { + progressCb = callback; +} + +void AppUpdater::updateProgress(UpdateStatus newStatus, int percentage, const char* message) { + status = newStatus; + if (progressCb) { + progressCb(status, percentage, message); + } +} + +bool AppUpdater::checkManifest() { + String url = String(bucketUrl) + manifestName; + ESP_LOGD(TAG, "Fetching manifest from: %s", url.c_str()); + + // Start the HTTP client and Send GET request for manifest + HTTPClient http; + http.begin(url); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "HTTP GET failed, error: %d", httpCode); + http.end(); + return false; + } + + // Read the response + String payload = http.getString(); + http.end(); + + // Parse JSON + DeserializationError error = deserializeJson(jsonManifest, payload); + if (error) { + ESP_LOGE(TAG, "Failed to parse manifest: %s", error.c_str()); + return false; + } + + // Check for version section + JsonObject jsonVersion = jsonManifest["version"]; + if (jsonVersion.isNull()) { + ESP_LOGE(TAG, "No version section in manifest"); + return false; + } + + // Get the remote version + int major = jsonVersion["major"] | 0; + int minor = jsonVersion["minor"] | 0; + int patch = jsonVersion["patch"] | 0; + Version remoteVersion = {major, minor, patch}; + + Version localVersion; + sscanf(currentVersion, "%d.%d.%d", &localVersion.major, &localVersion.minor, &localVersion.patch); + + // Check if an update is available + if (remoteVersion < localVersion) { + ESP_LOGI(TAG, "No updates available"); + return false; + } + + ESP_LOGD(TAG, "Manifest content: %s", payload.c_str()); + + return true; +} + +bool AppUpdater::updateFile(const char* remotePath, const char* localPath, const char* expectedMd5) { + updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); + + // Construct full URL + String url = String(bucketUrl) + remotePath; + ESP_LOGD(TAG, "Downloading: %s -> %s", url.c_str(), localPath); + + String localMd5 = getLocalMD5(localPath); + + if (localMd5.equals(expectedMd5)) { + ESP_LOGI(TAG, "File already up to date: %s", localPath); + updateProgress(UpdateStatus::SKIPPING, 100, localPath); + return true; + } + + // Start the download + HTTPClient http; + http.begin(url); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "Download failed: %d", httpCode); + updateProgress(UpdateStatus::ERROR, 0); + http.end(); + return false; + } + + // Get the stream and content length + WiFiClient* stream = http.getStreamPtr(); + size_t contentLength = http.getSize(); + + // Verify and save the file + bool success = verifyAndSaveFile(stream, contentLength, localPath, expectedMd5); + http.end(); + + updateProgress(success ? UpdateStatus::COMPLETE : UpdateStatus::ERROR, 100, localPath); + return success; +} + +bool AppUpdater::verifyAndSaveFile(WiFiClient* stream, size_t contentLength, const char* localPath, const char* expectedMd5) +{ + MD5Builder md5; + md5.begin(); + size_t totalRead = 0; + + // Create temporary filename + String tempPath = String(localPath) + ".tmp"; + + // Open temporary file for writing + File file = fileSystem.open(tempPath.c_str(), FILE_WRITE); + if (!file) { + ESP_LOGE(TAG, "Failed to open temporary file for writing"); + return false; + } + + updateProgress(UpdateStatus::DOWNLOADING, 0, localPath); + + // Single pass: Save file and calculate MD5 + while (totalRead < contentLength) { + size_t available = stream->available(); + if (available) { + size_t readLen = stream->readBytes(downloadBuffer.get(), std::min(available, size_t(BUFFER_SIZE))); + + // Write to temp file and update MD5 + if (file.write(downloadBuffer.get(), readLen) != readLen) { + ESP_LOGE(TAG, "Failed to write to temporary file"); + + file.close(); + fileSystem.remove(tempPath.c_str()); + return false; + } + + md5.add(downloadBuffer.get(), readLen); + totalRead += readLen; + updateProgress(UpdateStatus::DOWNLOADING, (totalRead * 90) / contentLength , localPath); + } + yield(); + } + + file.close(); + md5.calculate(); + String calculatedMd5 = md5.toString(); + + // Verify MD5 hash + if (!calculatedMd5.equals(expectedMd5)) { + ESP_LOGE(TAG, "MD5 mismatch for %s", localPath); + fileSystem.remove(tempPath.c_str()); + return false; + } + + // Replace original file with verified temp file + if (fileSystem.exists(localPath)) { + fileSystem.remove(localPath); + } + if (!fileSystem.rename(tempPath.c_str(), localPath)) { + ESP_LOGE(TAG, "Failed to rename temporary file"); + fileSystem.remove(tempPath.c_str()); + return false; + } + + updateProgress(UpdateStatus::COMPLETE, 100, localPath); + return true; +} + +String AppUpdater::getLocalMD5(const char* filePath){ + File file = fileSystem.open(filePath, "r"); + if(!file){ + ESP_LOGE(TAG, "Error opening %s...", filePath); + return String(); + } + + MD5Builder md5Builder; + md5Builder.begin(); + size_t fileSize = file.size(); + size_t totalRead = 0; + size_t readLen = 0; + while (totalRead < fileSize) { + readLen = file.readBytes(reinterpret_cast(downloadBuffer.get()), std::min(fileSize - totalRead, size_t(BUFFER_SIZE))); + md5Builder.add(downloadBuffer.get(), readLen); + totalRead += readLen; + } + + md5Builder.calculate(); + file.close(); + return md5Builder.toString(); +} + +bool AppUpdater::updateFilesArray() { + int successCount = 0; + int totalFiles = jsonFilesArray.size(); + ESP_LOGI(TAG, "Found %d files in manifest", totalFiles); + + // Iterate over each file entry in the manifest + for (JsonObject file : jsonFilesArray) { + const char* remotePath = file["remote"]; + const char* localPath = file["local"]; + const char* expectedMd5 = file["md5"]; + + // Skip invalid entries + if (!remotePath || !localPath || !expectedMd5) { + ESP_LOGE(TAG, "Invalid file entry in manifest"); + continue; + } + + // Attempt to update the file + if (updateFile(remotePath, localPath, expectedMd5)) { + successCount++; + } + } + + ESP_LOGI(TAG, "Manifest update complete: %d/%d files updated", successCount, totalFiles); + return successCount == totalFiles; +} + +bool AppUpdater::updateApp() { + updateProgress(UpdateStatus::MESSAGE, 0, "Starting firmware update"); + + // Check for firmware section in manifest + if (!jsonManifest["firmware"].is() || !jsonManifest["firmware"]["md5"].is()) { + ESP_LOGE(TAG, "Invalid firmware section in manifest"); + updateProgress(UpdateStatus::MESSAGE, 0, "Firmware: Invalid firmware section in manifest"); + return false; + } + + // Get the firmware MD5 hash and URL + const char* expectedMd5 = jsonManifest["firmware"]["md5"]; + String firmwareUrl = String(bucketUrl) + appName; + + // Download the firmware + HTTPClient http; + http.begin(firmwareUrl); + int httpCode = http.GET(); + if (httpCode != HTTP_CODE_OK) { + ESP_LOGE(TAG, "Firmware download failed: %d", httpCode); + updateProgress(UpdateStatus::MESSAGE, 0, "Firmware: Firmware download failed"); + http.end(); + return false; + } + + // Check available space + size_t firmwareSize = http.getSize(); + if (!Update.begin(firmwareSize)) { + ESP_LOGE(TAG, "Firmware: Not enough space for update"); + updateProgress(UpdateStatus::MESSAGE, 0, "Firmware: Not enough space for update"); + http.end(); + return false; + } + + // Set up MD5 checking + MD5Builder md5; + md5.begin(); + + // Download and verify firmware + WiFiClient* stream = http.getStreamPtr(); + size_t remaining = firmwareSize; + while (remaining > 0) { + size_t chunk = std::min(remaining, size_t(BUFFER_SIZE)); + size_t read = stream->readBytes(downloadBuffer.get(), chunk); + + // Check for timeout + if (read == 0) { + ESP_LOGE(TAG, "Read timeout"); + + Update.abort(); + http.end(); + return false; + } + + // Update MD5 and write firmware + md5.add(downloadBuffer.get(), read); + if (Update.write(downloadBuffer.get(), read) != read) { + ESP_LOGE(TAG, "Write failed"); + Update.abort(); + http.end(); + return false; + } + + remaining -= read; + updateProgress(UpdateStatus::DOWNLOADING, (firmwareSize - remaining) * 100 / firmwareSize, "firmware"); + } + + // Verify MD5 + md5.calculate(); + String calculatedMd5 = md5.toString(); + if (!calculatedMd5.equals(expectedMd5)) { + ESP_LOGE(TAG, "MD5 mismatch. Expected: %s, Got: %s", expectedMd5, calculatedMd5.c_str()); + updateProgress(UpdateStatus::MESSAGE, 0, "Firmware: MD5 mismatch"); + Update.abort(); + http.end(); + return false; + } + + // Finish update + if (!Update.end()) { + ESP_LOGE(TAG, "Update end failed"); + updateProgress(UpdateStatus::MESSAGE, 0, "Firmware: Update failed"); + http.end(); + return false; + } + + http.end(); + return true; +} + +bool AppUpdater::IsUpdateAvailable(){ + return updateAvailable; +} + + + +//AsyncEventSource* eventSource = nullptr; +AsyncWebSocket* wsSource = nullptr; + +void startFirmwareUpdateTask(AsyncWebSocket* wsSrc) { + wsSource = wsSrc; + if(Update_Task_Handle) { + ESP_LOGW(TAG, "Firmware update task already running"); + return; + } + xTaskCreate(firmwareUpdateTask, "FirmwareUpdate", 8192, NULL, 1, &Update_Task_Handle); +} + +void firmwareUpdateTask(void* parameter) { + static const char* TAG = "UpdateTask"; + String updateJsonPath = "/system/update.json"; + AppUpdater* updater = nullptr; + + try { + // Read and parse update.json + File file = LittleFS.open(updateJsonPath); + if (!file) { + throw std::runtime_error("Failed to open update.json"); + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { throw std::runtime_error("Failed to parse update.json"); } + + // Get update configuration + JsonObject jObj = doc.as(); + String baseUrl = jsonConstrainString(TAG, jObj, "baseurl", "https://storage.googleapis.com/boothifier/"); + String folderName = jsonConstrainString(TAG, jObj, "folder", "latest/"); + String url = baseUrl + folderName; + + // Initialize updater + updater = new AppUpdater(LittleFS, FIRMWARE_VERSION, url.c_str(), "update.json", "firmware.bin"); + updater->setProgressCallback(updateProgress); + + ESP_LOGI(TAG, "Starting update check from: %s", url.c_str()); + + + // Test loop + for(int i = 0; i < 10; i++) { + updateProgress(AppUpdater::UpdateStatus::MESSAGE, i * 10, "test"); + vTaskDelay(2000); + } + updateProgress(AppUpdater::UpdateStatus::MESSAGE, 100, "test complete"); + goto end; + + // Check and perform updates + if (!updater->checkManifest()) { throw std::runtime_error("Failed to check manifest"); } + + if (updater->IsUpdateAvailable()) { + ESP_LOGI(TAG, "Update available, updating files..."); + + if (!updater->updateFilesArray()) { throw std::runtime_error("Failed to update files"); } + + ESP_LOGI(TAG, "Updating firmware..."); + if (!updater->updateApp()) { throw std::runtime_error("Failed to update firmware"); } + + ESP_LOGI(TAG, "Update successful, restarting..."); + delete updater; + vTaskDelay(2000); + ESP.restart(); + } + + } catch (const std::exception& e) { + ESP_LOGE(TAG, "Update failed: %s", e.what()); + } + end: + delete updater; + Update_Task_Handle = NULL; + vTaskDelete(NULL); +} + +void updateProgress(AppUpdater::UpdateStatus newStatus, int percentage, const char* message = nullptr) { + + char buffer[128]; + const char* msg; + bool isComplete = false; + + switch (newStatus) { + case AppUpdater::UpdateStatus::IDLE: + snprintf(buffer, sizeof(buffer), "Update idle"); + msg = buffer; + break; + case AppUpdater::UpdateStatus::MESSAGE: + msg = message ? message : ""; + break; + case AppUpdater::UpdateStatus::DOWNLOADING: + snprintf(buffer, sizeof(buffer), "%s: Download progress: %d%%", message, percentage); + msg = buffer; + break; + case AppUpdater::UpdateStatus::VERIFYING: + snprintf(buffer, sizeof(buffer), "%s: Verifying update: %d%%", message, percentage); + msg = buffer; + break; + case AppUpdater::UpdateStatus::SKIPPING: + snprintf(buffer, sizeof(buffer), "%s: Skipping file update: already up to date", message); + msg = buffer; + break; + case AppUpdater::UpdateStatus::SAVING: + snprintf(buffer, sizeof(buffer), "%s: Saving update: %d%%", message, percentage); + msg = buffer; + break; + case AppUpdater::UpdateStatus::COMPLETE: + snprintf(buffer, sizeof(buffer), "%s: Update complete", message); + msg = buffer; + isComplete = true; + break; + case AppUpdater::UpdateStatus::ERROR: + snprintf(buffer, sizeof(buffer), "%s: Update error occurred", message); + msg = buffer; + break; + default: + snprintf(buffer, sizeof(buffer), "Unknown update status: %d", (int)newStatus); + msg = buffer; + break; + } + + ESP_LOGI(TAG, "%s", msg); + sendUpdateMessage(msg, isComplete, percentage); +} + +void sendUpdateMessage(const char* message, bool complete, int progress = -1) { + if(wsSource && wsSource->count() > 0) { + JsonDocument jsonDoc; + jsonDoc["message"] = message; + jsonDoc["complete"] = complete; + jsonDoc["progress"] = progress; + String strMessage; + serializeJson(jsonDoc, strMessage); + wsSource->textAll(strMessage); + } +} + + + + + + +/* +void setup() { + Serial.begin(115200); + + // Initialize WiFi connection first + // ... WiFi connection code ... + + // Initialize filesystem + if(!LittleFS.begin()) { + Serial.println("LittleFS Mount Failed"); + return; + } + + // Create updater instance with: + // - Current version: "1.0.0" + // - Update server URL: "https://my-update-server.com/" + // - Filesystem: LittleFS + AppUpdater updater("1.0.0", "https://storage.googleapis.com/boothifier/latest/", LittleFS); + + // Set progress callback + updater.setProgressCallback([](int progress) { + Serial.printf("Update progress: %d%%\n", progress); + }); + + // Check and update firmware + if (updater.checkAndUpdate()) { + Serial.println("Update successful! Rebooting..."); + ESP.restart(); + } + + // Update specific files from manifest + int updatedFiles = updater.updateFilesFromManifest("test_update.json"); + Serial.printf("Updated %d files\n", updatedFiles); +} + +*/ \ No newline at end of file diff --git a/webSock/AppUpgrade.h b/webSock/AppUpgrade.h new file mode 100644 index 0000000..c23d9c0 --- /dev/null +++ b/webSock/AppUpgrade.h @@ -0,0 +1,156 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "AppVersion.h" + +#define DEFAULT_MANIFEST_URL "https://storage.googleapis.com/boothifier/latest/" +#define BUFFER_SIZE 4096 + +extern TaskHandle_t Update_Task_Handle; + +/** + * @brief File information structure + */ +struct FileInfo { + String remotePath; ///< Path on remote server + String localPath; ///< Path in local filesystem + String md5; ///< MD5 hash for verification + //size_t size; ///< File size in bytes +}; + +/** + * @class AppUpdater + * @brief Handles firmware and filesystem updates + */ +class AppUpdater { + public: + Version localVersion; + const char* bucketUrl; + const char* appName; + const char* manifestName; + JsonDocument jsonManifest; + JsonArray jsonFilesArray; + + /** + * @brief Update status enumeration + */ + enum class UpdateStatus { + IDLE, ///< No update in progress + MESSAGE, ///< Update message + DOWNLOADING, ///< Downloading files + VERIFYING, ///< Verifying file integrity + SKIPPING, ///< File already up to date + SAVING, ///< Saving to filesystem + COMPLETE, ///< Update complete + ERROR ///< Error occurred + }; + + /** + * @brief Constructor + * @param version Current firmware version + * @param bucket Base URL for updates + * @param fs Filesystem reference + */ + AppUpdater(fs::FS& fs, const char* currVersion, const char* bucket, const char* manifestName ="update.json", const char* appBin = "firmware.bin" ); + + /** + * @brief Set progress callback function + * @param callback Function to call with progress updates + */ + void setProgressCallback(void (*callback)( UpdateStatus status, int percentage, const char* message)); + + /** + * @brief Check for and apply updates + * @return true if update successful + */ + bool IsUpdateAvailable(void); + + /** + * @brief Update files from manifest + * @param manifestPath Path to manifest file + * @return true if all files updated successfully + */ + //bool updateFilesFromManifest(const char* manifestPath = DEFAULT_MANIFEST_URL); + bool updateFilesArray(void); + + /** + * @brief Update single file + * @param remotePath Remote file path + * @param localPath Local file path + * @param expectedMd5 Expected MD5 hash + * @return true if file updated successfully + */ + bool updateFile(const char* remotePath, const char* localPath, const char* expectedMd5); + + /** + * @brief Get manifest content + * @param manifestPath Path to manifest file + * @return Manifest content as a json document + */ + bool checkManifest(void); + + bool updateApp(void); + + String getVersion(void); + + private: + typedef void (*ProgressCallback)(UpdateStatus status, int percentage, const char* message); + ProgressCallback progressCb; + fs::FS& fileSystem; + UpdateStatus status; + std::unique_ptr downloadBuffer; + bool updateAvailable = false; + + + + /** + * @brief Verify and save file + * @param stream Input stream + * @param contentLength Expected content length + * @param localPath Local file path + * @param expectedMd5 Expected MD5 hash + * @return true if successful + */ + bool verifyAndSaveFile(WiFiClient* stream, size_t contentLength, + const char* localPath, const char* expectedMd5); + + /** + * @brief Update progress callback + * @param percentage Progress percentage + * @param newStatus Current status + */ + void updateProgress(UpdateStatus newStatus, int percentage, const char* message = nullptr); + + String getLocalMD5(const char* filePath); +}; + + + +// Queue handle for firmware update messages +extern QueueHandle_t updateMsgQueue; + +// Message structure for update progress +struct UpdateMessage { + String message; + bool complete; + int progress; +}; + +/** + * @brief Firmware update task + * @param param Task parameters + */ +void firmwareUpdateTask(void* param); + +void startFirmwareUpdateTask(AsyncWebSocket* eventSrc); + +void updateProgress(AppUpdater::UpdateStatus status, int percentage, const char* message); + +void sendUpdateMessage(const char* message, bool complete, int progress); + +void handleUpdateProgress(AsyncWebServerRequest *request); \ No newline at end of file diff --git a/webSock/my_wifi.cpp b/webSock/my_wifi.cpp new file mode 100644 index 0000000..36adfbf --- /dev/null +++ b/webSock/my_wifi.cpp @@ -0,0 +1,1164 @@ +#include "my_wifi.h" +#include +#include +#include +#include +#include +#include "esp_log.h" +#include +#include +#include +#include "common/fileSystem.h" +#include "global.h" +#include "my_buzzer.h" +#include +#include +#include "jsonconstrain.h" +#include "AppUpgrade.h" + +static const char *tag = "WIFI"; + +volatile bool WifiClientConnected = false; +AsyncWebServer webServer(80); +AsyncWebSocket wsUpgradeProgress("/upgrade-progress"); +//AsyncEventSource eventUpgradeProgress("/upgrade-progress"); +//DNSServer *dnsServer; +//#define DNS_PORT 53 + +String client_ssid; +String client_pass; +String ap_ssid; +String ap_pass; + +String mDnsName; +String HostName; + +IPAddress local_IP(192,168,10,1); +IPAddress gateway(192,168,10,1); +IPAddress subnet(255,255,255,0); + +// for file manager page +String filesDropdownOptions((char*)0); +String dirDropdownOptions((char*)0); +String savePath((char*)0); // needed for storing file when editing a file +String savePathInput((char*)0); +const char* http_username = "admin"; +const char* http_password = "admin"; +const char* param_delete_path = "delete-path"; +const char* param_edit_path = "edit-path"; +const char* param_dir_pad = "dir-path"; +const char* param_edit_textarea = "edit-textarea"; +const char* param_save_path = "save-path"; +String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg"; + +volatile bool scanInProgress = false; + +static String networkList = ""; +int networkCount = 0; + +volatile int scanStatus = 0; // 0=none, 1=scanning, 2=complete, -1=error +String scanResults = ""; // Store scan results globally + +TaskHandle_t Wifi_Task_Handle; + +void Wifi_Task(void* parameter) { + + for(;;) { + static String last_ssid = ""; + + if (!WifiClientConnected || last_ssid != client_ssid) { + WifiClientConnected = false; + ESP_LOGD(tag, "Wifi trying to connect to: %s", client_ssid.c_str()); + WiFi.disconnect(); + vTaskDelay(1000); + WiFi.setAutoReconnect(false); + WiFi.begin(client_ssid.c_str(), client_pass.c_str()); + vTaskDelay(1000); + // wait up to 10 iterations for connection + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 10) { + wl_status_t status = WiFi.status(); + switch (status) { + case WL_NO_SSID_AVAIL: + ESP_LOGW(tag, "No AP with SSID %s found", client_ssid.c_str()); + break; + case WL_CONNECT_FAILED: + ESP_LOGW(tag, "Connection failed (wrong password?)"); + break; + case WL_DISCONNECTED: + ESP_LOGD(tag, "Not connected yet... (attempt %d/10)", attempts + 1); + break; + default: + ESP_LOGD(tag, "Status: %d", status); + break; + } + vTaskDelay(2000); + attempts++; + } + + if(WiFi.status() == WL_CONNECTED){ + ESP_LOGD(tag, "Connected to %s", client_ssid.c_str()); + //mDnsName = WiFi.hostname(); + WifiClientConnected = true; + WiFi.setAutoReconnect(true); + last_ssid = client_ssid; + // Save the WiFi settings + Wifi_Save_Credentials("/system/wifi.json"); + } else { + ESP_LOGW(tag, "Failed to connect to %s", client_ssid.c_str()); + } + } + + vTaskSuspend(Wifi_Task_Handle); + } + + vTaskDelete(NULL); +} + +void Wifi_Save_Credentials(String path) { + // Load existing JSON + JsonDocument doc; + File readFile = LittleFS.open(path, "r"); + if (readFile) { + DeserializationError error = deserializeJson(doc, readFile); + readFile.close(); + if (error) { + ESP_LOGE(tag, "Failed to parse existing JSON"); + return; + } + } + + // Update or create wifi-client section + JsonObject wifiClient = doc["wifi-client"].to(); + wifiClient["ssid"] = client_ssid; + wifiClient["pass"] = client_pass; + + // Save updated JSON + File writeFile = LittleFS.open(path, "w"); + if (!writeFile) { + ESP_LOGE(tag, "Error opening %s for writing", path.c_str()); + return; + } + + // Serialize JSON with pretty formatting + if (serializeJsonPretty(doc, writeFile) == 0) { + ESP_LOGE(tag, "Failed to write JSON to file"); + } + writeFile.close(); +} + +void Wifi_Init() { + // Initialize LittleFS + if (!LittleFS.begin(true)) { + ESP_LOGE(tag, "LittleFS mount failed"); + return; + } + + // Set Wi-Fi task to run on Core 1 + esp_wifi_set_ps(WIFI_PS_NONE); // Disable power save mode for better responsiveness + + // Set WiFi to AP+STA mode + WiFi.mode(WIFI_MODE_APSTA); + + Wifi_Scan_for_Networks(); + + // Configure and start AP + WiFi.softAPConfig(local_IP, gateway, subnet); + if (!WiFi.softAP(ap_ssid, ap_pass)) { + ESP_LOGE(tag, "AP start failed"); + return; + } + + // Add CORS headers + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT"); + DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); + + Setup_WebServer_Handlers(webServer); + + WiFi.onEvent(onWiFiEvent); + WiFi.setHostname(mDnsName.c_str()); + + webServer.begin(); + ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str()); + + // Start the WiFi task + xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*4, NULL, 1, &Wifi_Task_Handle, 0); +} + +void Wifi_Load_Settings(String path){ + // Load WiFi settings + File file = LittleFS.open(path, "r"); + if (!file) { + ESP_LOGE(tag, "Error opening %s", path.c_str()); + return; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + ESP_LOGE(tag, "Failed to deserialize %s", path.c_str()); + return; + } + + JsonObject wifiJson = doc.as(); + if (wifiJson.isNull()) { + ESP_LOGE(tag, "%s is empty", path.c_str()); + return; + } + + // Load AP settings + JsonObject apJson = wifiJson["wifi-ap"]; + if (!apJson.isNull()) { + ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP"); + ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678"); + local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1")); + gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1")); + subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0")); + } + + // Load Client settings + JsonObject clientJson = wifiJson["wifi-client"]; + if (!apJson.isNull()) { + client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none"); + client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678"); + } +} + +void Wifi_Scan_for_Networks(){ + // Start a scan for available networks + WiFi.scanNetworks(false, false); + while (WiFi.scanComplete() == WIFI_SCAN_RUNNING) { + vTaskDelay(100); // Wait for scan to complete + } + + networkCount = WiFi.scanComplete(); + if (networkCount >= 0) { + JsonDocument doc; + JsonArray networks = doc["networks"].to(); + + for (int i = 0; i < networkCount; i++) { + auto network = networks.add(); + network["ssid"] = WiFi.SSID(i); + network["rssi"] = WiFi.RSSI(i); + network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN; + } + + String jsonString; + serializeJson(doc, jsonString); + networkList = jsonString; + WiFi.scanDelete(); + } else { + ESP_LOGE(tag, "WiFi scan failed"); + } +} + +void Setup_WebServer_Handlers(AsyncWebServer& server){ + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(LittleFS, "/www/index.html", "text/html"); + }); + server.on("/home", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); + }); + server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + //sendHtmlFile("/www/setup.html", request, htmlProcessor); + }); + server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/about.html", request, fileManagerHtmlProcessor); + //request->send(LittleFS, "/www/about.html", "text/html"); + }); + + + // Wifi related handlers + server.on("/wifi/connect", HTTP_POST, [](AsyncWebServerRequest *request){ + if (request->hasParam("ssid", false, false) && request->hasParam("pass", false, false)) { + client_ssid = request->getParam("ssid", false, false)->value().c_str(); + if (Wifi_Task_Handle != NULL && eTaskGetState(Wifi_Task_Handle) == eSuspended) { + ESP_LOGD(tag, "Resuming WiFi task"); + WifiClientConnected = false; + vTaskResume(Wifi_Task_Handle); + } else { + ESP_LOGE(tag, "Failed to resume WiFi task: invalid handle or task not suspended"); + } + vTaskResume(Wifi_Task_Handle); + request->send(200, "application/json", "{\"status\":\"connecting\"}"); + } else { + ESP_LOGE(tag, "Missing ssid or pass parameter"); + request->send(400, "application/json", "{\"error\":\"Missing ssid or pass parameter\"}"); + } + }); + server.on("/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request){ + String jsonStr = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + + "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}"; + request->send(200, "application/json", jsonStr); + }); + server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request){ + if(networkCount <= 0) { + request->send(400, "application/json", "{\"error\":\"No scan results\"}"); + return; + } + + request->send(200, "application/json", networkList); + }); + server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_MODE_APSTA){ + // TODO Disable navigation bar + } + //sendHtmlFile("/www/wifi.html", request, htmlProcessor); + request->send(LittleFS, "/www/wifi.html", "text/html"); + }); + + + // File Manager related handlers + server.on("/files/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handleFilesUpload_OnBody); + + server.on("/files/download", HTTP_GET, [](AsyncWebServerRequest *request){ + if (!request->hasParam("file")) { + ESP_LOGE(tag, "Missing file parameter"); + request->send(400, "text/plain", "Missing file parameter"); + return; + } + + try { + String filename = uriDecode(request->getParam("file")->value()); + ESP_LOGD(tag, "Download request for: %s", filename.c_str()); + request->send(LittleFS, filename, "application/octet-stream"); + } + catch (const std::exception& e) { + ESP_LOGE(tag, "Download failed: %s", e.what()); + request->send(404, "text/plain", "File not found!"); + } + }); + server.on("/files/delete", HTTP_GET, [](AsyncWebServerRequest *request) { + static const char* tag = "DeleteHandler"; + + // Authentication check + if (!request->authenticate(http_username, http_password)) { + ESP_LOGW(tag, "Authentication failed for delete request"); + return request->requestAuthentication(); + } + + // Parameter validation + if (!request->hasParam(param_delete_path)) { + ESP_LOGE(tag, "Missing delete path parameter"); + request->send(400, "text/plain", "Missing file path"); + return; + } + + // Get and validate filename + String filename = uriDecode(request->getParam(param_delete_path)->value()); + if (filename == "choose") { + request->redirect("/files"); + return; + } + + // Ensure path starts with / + if (!filename.startsWith("/")) { + filename = "/" + filename; + } + + try { + if (LittleFS.remove(filename.c_str())) { + ESP_LOGI(tag, "Successfully deleted file: %s", filename.c_str()); + } else { + ESP_LOGE(tag, "Failed to delete file: %s", filename.c_str()); + request->send(500, "text/plain", "Delete failed"); + return; + } + } catch (const std::exception& e) { + ESP_LOGE(tag, "Exception during delete: %s", e.what()); + request->send(500, "text/plain", "Delete failed"); + return; + } + + request->redirect("/files"); + }); + server.on("/files/edit", HTTP_GET, [](AsyncWebServerRequest *request) { + static const char* tag = "EditHandler"; + + // Authentication check + if (!request->authenticate(http_username, http_password)) { + ESP_LOGW(tag, "Authentication failed"); + return request->requestAuthentication(); + } + + // Parameter validation + if (!request->hasParam(param_edit_path)) { + ESP_LOGE(tag, "Missing edit path parameter"); + request->send(400, "text/plain", "Missing file path"); + return; + } + + // Get and decode filename + String fileName = uriDecode(request->getParam(param_edit_path)->value()); + ESP_LOGD(tag, "Edit request for file: %s", fileName.c_str()); + + // Set save path + savePath = (fileName == "new") ? "/new.txt" : fileName; + + // Validate path + if (!savePath.startsWith("/")) { + savePath = "/" + savePath; + } + + ESP_LOGD(tag, "Save path set to: %s", savePath.c_str()); + + try { + sendHtmlFile("/www/edit.html", request, fileManagerHtmlProcessor); + } catch (const std::exception& e) { + ESP_LOGE(tag, "Failed to send edit page: %s", e.what()); + request->send(500, "text/plain", "Internal server error"); + } + }); + server.on("/files/save", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + String inputMessage((char*)0); + if (request->hasParam(param_edit_textarea)) { + inputMessage = request->getParam(param_edit_textarea)->value(); + } + if (request->hasParam(param_save_path)) { + savePath = uriDecode(request->getParam(param_save_path)->value()); + } + writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); + + request->redirect("/files"); + }); + server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/files.html", request, fileManagerHtmlProcessor); + }); + + + // Lights related handlers + server.on("/lights/settings", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200); + }); + server.on("/lights/settings", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200); + }); + server.on("/lights/animation", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200); + }); + server.on("/lights/setpixel", HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200); + }); + + + // System and LED related handlers + server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request){ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request){ + /* + //CreateSysSummmaryPacket(doc); + String summary; + serializeJson(jsDoc, summary); + request->send(200, "application/json", summary); + */ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request){ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + + + + // LightStik related handlers + server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request){ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request){ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request){ + String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; + request->send(200, "application/json", response); + }); + + + // Firmware Update Handlers + server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request) { + JsonDocument doc; + doc["currentVersion"] = FIRMWARE_VERSION; + doc["latestVersion"] = "1.1.0"; + doc["updateAvailable"] = true; + + String response; + serializeJson(doc, response); + request->send(200, "application/json", response); + }); + // Start update process + server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request) { + //if (!request->authenticate(http_username, http_password)) { + // return request->requestAuthentication(); + //} + startFirmwareUpdateTask(&wsUpgradeProgress); + request->send(200); + }); + //server.on("/upgrade/progress", HTTP_GET, [](AsyncWebServerRequest *request) { + /* + if (!request->authenticate(http_username, http_password)) { + return request->requestAuthentication(); + } + AsyncWebServerResponse *response = request->beginResponse(200, "text/event-stream"); + response->addHeader("Cache-Control", "no-cache"); + response->addHeader("Connection", "keep-alive"); + request->send(response); + */ + //}); + server.on("/upgrade", HTTP_GET, [](AsyncWebServerRequest *request) { + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + request->send(LittleFS, "/www/upgrade.html", "text/html"); + }); + + wsUpgradeProgress.onEvent(onWsUpdateProgressEvent); + server.addHandler(&wsUpgradeProgress); + + // Basic Connection status check + server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"status\":\"connected\"}"); + }); + // Server requested files that aren't template processed + server.on("/*", HTTP_GET, [](AsyncWebServerRequest *request) { // handle file uploads + // Validate request + if (!request) { + ESP_LOGE(tag, "Invalid request"); + return; + } + + // Get and validate file path + String filePath = request->url(); + if (filePath.isEmpty()) { + ESP_LOGE(tag, "Empty file path"); + request->send(400, "text/plain", "Invalid file path"); + return; + } + + // Ensure path starts with '/' + if (!filePath.startsWith("/")) { + filePath = "/" + filePath; + } + + try { + // Get content type once + const char* contentType = getFileType(getFileExtension(filePath.c_str())); + if (!contentType) { + ESP_LOGW(tag, "Unknown file type: %s", filePath.c_str()); + contentType = "application/octet-stream"; + } + + ESP_LOGD(tag, "Sending file: %s (%s)", filePath.c_str(), contentType); + request->send(LittleFS, filePath, contentType); + } + catch (const std::runtime_error& e) { + ESP_LOGE(tag, "FileSystem error: %s for path: %s", e.what(), filePath.c_str()); + request->send(404, "text/plain", "File not found"); + } + catch (const std::exception& e) { + ESP_LOGE(tag, "Error: %s for path: %s", e.what(), filePath.c_str()); + request->send(500, "text/plain", "Internal server error"); + } + }); + // 404 handler + server.onNotFound([](AsyncWebServerRequest *request) { + ESP_LOGE(tag, "404: %s", request->url().c_str()); + request->send(404, "text/plain", "Not found"); + }); + +} + +void onWsUpdateProgressEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { + switch (type) { + case WS_EVT_CONNECT: + ESP_LOGD(tag,"WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); + break; + case WS_EVT_DISCONNECT: + ESP_LOGD(tag, "WebSocket client #%u disconnected\n", client->id()); + break; + case WS_EVT_DATA: + ESP_LOGD(tag, "WebSocket client #%u data\n", client->id()); + //handleWebSocketMessage(arg, data, len); + break; + case WS_EVT_PONG: + ESP_LOGD(tag, "WebSocket client #%u pong\n", client->id()); + break; + case WS_EVT_ERROR: + ESP_LOGD(tag, "WebSocket client #%u error\n", client->id()); + break; + } +} + + + +void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit + + if (!index) { + // Initial upload chunk + if (!request->hasParam("dir-path", true, false)) { + ESP_LOGE(tag, "Missing dir-path parameter"); + request->send(400, "text/plain", "Missing dir-path"); + return; + } + + AsyncWebParameter* p = request->getParam("dir-path", true, false); + String path = p->value() + "/" + filename; + ESP_LOGD(tag, "Starting upload: %s", path.c_str()); + + // Validate path + if (!path.startsWith("/")) { + path = "/" + path; + } + + request->_tempFile = LittleFS.open(path, "w"); + if (!request->_tempFile) { + ESP_LOGE(tag, "Failed to create file: %s", path.c_str()); + request->send(500, "text/plain", "Failed to create file"); + return; + } + } + + // Write chunk + if (len && request->_tempFile) { + if (index + len > MAX_UPLOAD_SIZE) { + request->_tempFile.close(); + LittleFS.remove(request->_tempFile.name()); + ESP_LOGE(tag, "Upload too large"); + request->send(413, "text/plain", "File too large"); + return; + } + + if (!request->_tempFile.write(data, len)) { + ESP_LOGE(tag, "Write failed"); + request->_tempFile.close(); + request->send(500, "text/plain", "Write failed"); + return; + } + } + + if (final) { + request->_tempFile.close(); + ESP_LOGD(tag, "Upload complete: %s, %u bytes", filename.c_str(), index + len); + request->redirect("/files"); + } +} + + +// Send html file with template processing {{VAR}} +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)) { + try { + const char* htmlFile = readFile(LittleFS, filePath); + if (!htmlFile) { + ESP_LOGE(tag, "Failed to read file: %s", filePath); + request->send(404, "text/plain", "File not found"); + return; + } + + String processedData = varReplace(htmlFile, callback); + delete[] htmlFile; // Clean up allocated memory + + ESP_LOGD(tag, "Sent file: %s", filePath); + request->send(200, "text/html", processedData); + } + catch (const std::exception& e) { + ESP_LOGE(tag, "Error processing file %s: %s", filePath, e.what()); + request->send(500, "text/plain", "Server error"); + } +} + +const char* getFileExtension(const char* filename) { + // Input validation + if (!filename) { + ESP_LOGW(tag, "Null filename provided"); + return ""; + } + + // Find last dot + const char* lastDot = strrchr(filename, '.'); + if (!lastDot || lastDot == filename || *(lastDot + 1) == '\0') { + ESP_LOGD(tag, "No valid extension found in: %s", filename); + return ""; + } + + ESP_LOGD(tag, "Found extension: %s", lastDot + 1); + return lastDot + 1; +} + +const char* getFileType(const char* ext) { + if (!ext) return "application/octet-stream"; + + ESP_LOGD(tag, "Getting file type for extension: %s", ext); + + if (strcmp(ext, "png") == 0) return "image/png"; + if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return "image/jpeg"; + if (strcmp(ext, "gif") == 0) return "image/gif"; + if (strcmp(ext, "ico") == 0) return "image/x-icon"; + if (strcmp(ext, "txt") == 0) return "text/plain"; + if (strcmp(ext, "css") == 0) return "text/css"; + if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) return "text/html"; + if (strcmp(ext, "js") == 0) return "text/javascript"; + if (strcmp(ext, "json") == 0) return "application/json"; + + ESP_LOGW(tag, "Unknown file extension: %s", ext); + return "application/octet-stream"; +} + +// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content +String varReplace(const String& input, String (*callback)(const String&)) { + static const char* tag = "varReplace"; + + // Validate inputs + if (input.isEmpty() || !callback) { + ESP_LOGW(tag, "Empty input or null callback"); + return input; + } + + // Pre-allocate result string with estimated size + String result; + result.reserve(input.length() * 1.2); // Add 20% for potential replacements + + const int maxSegmentLength = 32; + int startPos = 0; + + // Process all segments + while (true) { + // Find next variable + int start = input.indexOf("{{", startPos); + if (start == -1) { + break; + } + + // Add text before variable + result += input.substring(startPos, start); + + // Find end of variable + int end = input.indexOf("}}", start + 2); + if (end == -1) { + ESP_LOGW(tag, "Unmatched {{ at position %d", start); + result += input.substring(start); + break; + } + + // Extract and validate segment + String segment = input.substring(start + 2, end); + if (segment.length() <= maxSegmentLength) { + try { + String replacement = callback(segment); + result += replacement; + } catch (const std::exception& e) { + ESP_LOGE(tag, "Callback error: %s", e.what()); + result += input.substring(start, end + 2); + } + } else { + ESP_LOGW(tag, "Segment too long: %d chars", segment.length()); + result += input.substring(start, end + 2); + } + + startPos = end + 2; + } + + // Add remaining text + if (startPos < input.length()) { + result += input.substring(startPos); + } + + return result; +} + +const char* convertFileSize(const size_t bytes) { + static char fileSizeBuffer[16]; // Pre-allocated buffer for the file size + + if (bytes < 1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes); + } else if (bytes < 1024 * 1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", bytes / 1024.0); + } else if (bytes < 1024 * 1024 * 1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", bytes / (1024.0 * 1024.0)); + } else { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); + } + + return fileSizeBuffer; +} + +void onWiFiEvent(WiFiEvent_t event) { + //Serial.printf("[WiFi-event] event: %d\n", event); + switch (event) { + case ARDUINO_EVENT_WIFI_READY: + ESP_LOGD(tag, "WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + ESP_LOGD(tag,"Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + ESP_LOGD(tag,"WiFi client started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + ESP_LOGD(tag,"WiFi clients stopped"); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + ESP_LOGD(tag,"Connected to AP"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + WifiClientConnected = false; + ESP_LOGD(tag, "WiFi Disconnected"); + Buzzer_Play_Tune(TUNE_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + ESP_LOGD(tag,"Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + WifiClientConnected = true; + ESP_LOGD(tag,"My IP: %s", WiFi.localIP().toString()); + //Wifi_Start_MDNS(); + Buzzer_Play_Tune(TUNE_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + WifiClientConnected = false; + ESP_LOGD(tag,"Lost IP address and IP address is reset to 0"); + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + ESP_LOGD(tag,"WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + ESP_LOGD(tag,"WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + ESP_LOGD(tag,"WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + ESP_LOGD(tag,"WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + ESP_LOGD(tag, "WiFi access point started"); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + ESP_LOGD(tag, "WiFi access point stopped"); + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + ESP_LOGD(tag, "Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + ESP_LOGD(tag, "SoftAP Client Disconnected"); + Buzzer_Play_Tune(TUNE_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + ESP_LOGD(tag,"SoftAP Client Connected"); + Buzzer_Play_Tune(TUNE_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + ESP_LOGD(tag,"Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + ESP_LOGD(tag,"AP IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: + ESP_LOGD(tag,"STA IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_GOT_IP6: + ESP_LOGD(tag,"Ethernet IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_START: + ESP_LOGD(tag,"Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + ESP_LOGD(tag,"Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + ESP_LOGD(tag,"Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + ESP_LOGD(tag,"Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + ESP_LOGD(tag,"Obtained IP address"); + break; + default: break; + } +} + +void Wifi_Start_MDNS(void) { + ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str()); + if (!MDNS.begin(mDnsName.c_str())) { + ESP_LOGE(tag, "Error setting up MDNS responder!"); + }else{ + ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName); + } +} + +bool writeFile(fs::FS &fs, const char* path, const char* message) { + // Validate inputs + if (!path || !message) { + ESP_LOGE(tag, "Invalid parameters: path=%p message=%p", path, message); + return false; + } + + // Open file with error checking + File file = fs.open(path, "w"); + if (!file) { + ESP_LOGE(tag, "Failed to open file: %s", path); + return false; + } + + // Write with error handling + try { + size_t bytesWritten = file.print(message); + if (bytesWritten == 0) { + ESP_LOGE(tag, "Failed to write to file: %s", path); + file.close(); + return false; + } + + // Ensure all data is written + file.flush(); + file.close(); + ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path); + return true; + } + catch (const std::exception& e) { + ESP_LOGE(tag, "Exception while writing file %s: %s", path, e.what()); + file.close(); + return false; + } +} + +char* readFile(fs::FS &fs, const char* path) { + static const char* tag = "readFile"; + static const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB limit + + // Validate input + if (!path) { + ESP_LOGE(tag, "Invalid path parameter"); + return nullptr; + } + + // Open file + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) { + ESP_LOGE(tag, "Failed to open file: %s", path); + return nullptr; + } + + // Check file size + size_t fileSize = file.size(); + if (fileSize == 0 || fileSize > MAX_FILE_SIZE) { + ESP_LOGE(tag, "Invalid file size: %u bytes", fileSize); + file.close(); + return nullptr; + } + + // Allocate memory + char* fileContent = new (std::nothrow) char[fileSize + 1]; + if (!fileContent) { + ESP_LOGE(tag, "Memory allocation failed for size: %u", fileSize + 1); + file.close(); + return nullptr; + } + + // Read file + size_t bytesRead = file.readBytes(fileContent, fileSize); + file.close(); + + if (bytesRead != fileSize) { + ESP_LOGE(tag, "Read failed: expected %u bytes, got %u", fileSize, bytesRead); + delete[] fileContent; + return nullptr; + } + + // Null terminate + fileContent[bytesRead] = '\0'; + ESP_LOGD(tag, "Successfully read %u bytes from %s", bytesRead, path); + return fileContent; +} + +String getSoftAPMacAddress() { + uint8_t mac[6]; + WiFi.softAPmacAddress(mac); + + String macString = ""; + for (int i = 0; i < 6; i++) { + macString += String(mac[i], HEX); + if (i < 5) macString += ":"; + } + return macString; +} + +String listDirAsHtml(String directoryList[], int count) { + String listedFiles; + + for (int i = 0; i < count; i++) { + // directory html + listedFiles += "Dir: "; + listedFiles += directoryList[i]; + listedFiles += "/-\n"; + + filesDropdownOptions += "\n"; + + dirDropdownOptions += "\n"; + + File dir = LittleFS.open(directoryList[i]); + File file = dir.openNextFile(); + while (file) { + String fileName = file.name(); + if (!file.isDirectory()) { + //Serial.println(" File: " + String(file.name())); + listedFiles += "  "; + listedFiles += fileName; + listedFiles += ""; + listedFiles += convertFileSize(file.size()); + listedFiles += "\n"; + + filesDropdownOptions += "\n"; + } + file = dir.openNextFile(); + } + dir.close(); + } + + return listedFiles; +} + + + +/******************** Specific Html Processors ********************/ +// file manager html processor +String fileManagerHtmlProcessor(const String& var){ + if(var == "ALLOWED_EXTENSIONS_EDIT"){ return allowedExtensionsForEdit; } + + if(var == "FS_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); } + + if(var == "FS_USED_BYTES"){ return convertFileSize(LittleFS.usedBytes()); } + + if(var == "FS_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "RAM_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "RAM_USED_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "RAM_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "LISTED_FILES"){ + filesDropdownOptions = ""; // clear out + dirDropdownOptions = ""; // clear out + String directories[MAX_DIRECTORIES]; + int dirCount = 0; + getAllDirectories(directories, dirCount); + return listDirAsHtml(directories, dirCount); + } + + if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; } + + if(var == "DIR_LIST"){ return dirDropdownOptions; } + + if(var == "SAVE_PATH_INPUT"){ return savePath; } + + if(var == "FIRM_VER"){ return FIRMWARE_VERSION; } + + return var; +} + +String HomeHtmlProcessor(const String& var) { + if (var == "APP_NAME") { + //return sysProps.appName; + return "N/A"; + } + if (var == "OLED") { + return "N/A"; + } + if (var == "STRIP1") { + //return (strip1) ? "Yes" : "No"; + return "N/A"; + } + if (var == "STRIP2") { + //return (strip2) ? "Yes" : "No"; + return "N/A"; + } + if (var == "FRONT_LIGHT") { + //return (animProps.frontLight.enabled) ? "Yes" : "No"; + return "N/A"; + } + if (var == "REAR_LIGHT") { + //return (animProps.rearLight.enabled) ? "Yes" : "No"; + return "N/A"; + } + if (var == "FIRMWARE") { + return FIRMWARE_VERSION; + } + if (var == "BOOTH_T") { + //return String(sysProps.t_sensor.temperature) + "F"; + return "N/A"; + } + if (var == "SETPOINT") { + //return String(sysProps.t_sensor.Setpoint1) + "F"; + return "N/A"; + } + if (var == "FLASH_SIZE") { return convertFileSize(ESP.getSketchSize());} + if (var == "FLASH_FREE") { return convertFileSize(ESP.getFreeSketchSpace());} + if (var == "HEAP_SIZE") { return convertFileSize(ESP.getHeapSize()); } + if (var == "HEAP_FREE") { return convertFileSize(ESP.getFreeHeap()); } + if (var == "CPU_FREQ") { return String(ESP.getCpuFreqMHz()) + "Mhz"; } + if (var == "IP") { return WiFi.localIP().toString(); } + if (var == "MAC") { return chipInfo.macStr; } + if (var == "SSID") { return WiFi.SSID(); } + if (var == "RSSI") { return String(WiFi.RSSI()); } + if (var == "WIFI_CH") { return String(WiFi.channel()); } + if (var == "ENCRYP") { return String(WiFi.encryptionType(0)); } + if (var == "AP_SSID") { return WiFi.softAPSSID(); } + if (var == "AP_CLIENTS") { return String(WiFi.softAPgetStationNum()); } + if (var == "BLE") { return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No"; } + if (var == "BLE_SSID") { + //return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : ""; + return "N/A"; + } + if (var == "BLE_CLIENTS") { + //return (BTDeviceConnected) ? "1" : "0"; + return "N/A"; + } + if (var == "AP_MAC") { return getSoftAPMacAddress(); } + + // Return an empty string if the variable is not recognized + return var; +} + + + +void handleUpdateProgress(AsyncWebServerRequest *request) { + static const char* tag = "UpdateProgress"; + + //if (!request->authenticate(http_username, http_password)) { + // return request->requestAuthentication(); + //} + + request->send(200, "text/plain", "Update progress"); // Send a simple response +} diff --git a/webSock/my_wifi.h b/webSock/my_wifi.h new file mode 100644 index 0000000..603d416 --- /dev/null +++ b/webSock/my_wifi.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +void Wifi_Init(void); +void Wifi_Load_Settings(String path); +void Wifi_Scan_for_Networks(void); +void Wifi_Start_MDNS(void); +void onWiFiEvent(WiFiEvent_t event); +void Wifi_Save_Credentials(String path); +void Setup_WebServer_Handlers(AsyncWebServer& serv); + +void handlePOST_Update(AsyncWebServerRequest *request); +void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); +void updateFirmwareProgress(size_t progress, size_t total); + +void handleGET_Query(AsyncWebServerRequest *request); + +void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); + +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)); +String fileManagerHtmlProcessor(const String& var); +String HomeHtmlProcessor(const String& var); +String listDirAsHtml(String directoryList[], int count); + +const char* getFileExtension(const char* filename); +const char* getFileType(const char* ext); +const char* convertFileSize(const size_t bytes); +bool writeFile(fs::FS &fs, const char *path, const char *message); +char* readFile(fs::FS &fs, const char *path); + +String varReplace(const String& input, String (*callback)(const String&)); +String getSoftAPMacAddress(void); + +void onWsUpdateProgressEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len); diff --git a/webSock/upgrade.html b/webSock/upgrade.html new file mode 100644 index 0000000..c0b55ea --- /dev/null +++ b/webSock/upgrade.html @@ -0,0 +1,193 @@ + + + + + + Firmware Upgrade + + + + +
+

Firmware Upgrade

+ +
+ + Disconnected +
+ +
+

Current Version: -

+

Latest Version: -

+
+ +
+ + +
+ +
+
+ + + + \ No newline at end of file diff --git a/web_ble.html b/web_ble.html new file mode 100644 index 0000000..b8309a0 --- /dev/null +++ b/web_ble.html @@ -0,0 +1,113 @@ + + + + + + BLE Interaction + + + +

BLE Interaction

+ + + + + + + + diff --git a/wokwi.toml b/wokwi.toml new file mode 100644 index 0000000..0587e7d --- /dev/null +++ b/wokwi.toml @@ -0,0 +1,6 @@ +[wokwi] +version = 1 +firmware = '\.pio\build\esp32s3dev\firmware.bin' +elf = '\.pio\build\esp32s3dev\firmware.elf' +rfc2217ServerPort = 4000 +gdbServerPort=3333 \ No newline at end of file diff --git a/z_old/HSVTable.cpp b/z_old/HSVTable.cpp new file mode 100644 index 0000000..a5643e7 --- /dev/null +++ b/z_old/HSVTable.cpp @@ -0,0 +1,175 @@ +#include "HSVTable.h" +#include +#include "LEDStrip.h" + + +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}; + +const uint8_t HSVlights[61] = +{0, 4, 8, 13, 17, 21, 25, 30, 34, 38, 42, 47, 51, 55, 59, 64, 68, 72, 76, +81, 85, 89, 93, 98, 102, 106, 110, 115, 119, 123, 127, 132, 136, 140, 144, +149, 153, 157, 161, 166, 170, 174, 178, 183, 187, 191, 195, 200, 204, 208, +212, 217, 221, 225, 229, 234, 238, 242, 246, 251, 255}; + +const uint8_t HSVpower[121] = +{0, 2, 4, 6, 8, 11, 13, 15, 17, 19, 21, 23, 25, 28, 30, 32, 34, 36, 38, 40, +42, 45, 47, 49, 51, 53, 55, 57, 59, 62, 64, 66, 68, 70, 72, 74, 76, 79, 81, +83, 85, 87, 89, 91, 93, 96, 98, 100, 102, 104, 106, 108, 110, 113, 115, 117, +119, 121, 123, 125, 127, 130, 132, 134, 136, 138, 140, 142, 144, 147, 149, +151, 153, 155, 157, 159, 161, 164, 166, 168, 170, 172, 174, 176, 178, 181, +183, 185, 187, 189, 191, 193, 195, 198, 200, 202, 204, 206, 208, 210, 212, +215, 217, 219, 221, 223, 225, 227, 229, 232, 234, 236, 238, 240, 242, 244, +246, 249, 251, 253, 255}; + +// the real HSV rainbow +rgbpixel_t trueHSV(int angle) +{ + byte red, green, blue; + + if (angle<60) {red = 255; green = HSVlights[angle]; blue = 0;} else + if (angle<120) {red = HSVlights[120-angle]; green = 255; blue = 0;} else + if (angle<180) {red = 0, green = 255; blue = HSVlights[angle-120];} else + if (angle<240) {red = 0, green = HSVlights[240-angle]; blue = 255;} else + if (angle<300) {red = HSVlights[angle-240], green = 0; blue = 255;} else + {red = 255, green = 0; blue = HSVlights[360-angle];} + + //return {red, green, blue}; + return {red, green, blue}; +} + +// the 'power-conscious' HSV rainbow +rgbpixel_t powerHSV(int angle) +{ + byte red, green, blue; + if (angle<120) {red = HSVpower[120-angle]; green = HSVpower[angle]; blue = 0;} else + if (angle<240) {red = 0; green = HSVpower[240-angle]; blue = HSVpower[angle-120];} else + {red = HSVpower[angle-240]; green = 0; blue = HSVpower[360-angle];} + + return {red, green, blue}; +} + +// sine wave rainbow +rgbpixel_t sineHSV(int angle) +{ + return {lights[(angle+120)%360], lights[angle], lights[(angle+240)%360]}; +} + + +// hsv out: 0-360, input rgb 0-255 +float RGBToHSV(rgbpixel_t rgb) { + float delta; // min; + float h = 0, v; //, s; + + uint8_t min_ = std::min(std::min(rgb.red, rgb.grn), rgb.blu); + v = std::max(std::max(rgb.red, rgb.grn), rgb.blu); + delta = v - min_; + + if (rgb.red == v){ + h = (rgb.grn - rgb.blu) / delta; + }else if (rgb.grn == v){ + h = 2 + (rgb.blu - rgb.red) / delta; + }else if (rgb.blu == v){ + h = 4 + (rgb.red - rgb.grn) / delta; + } + + h *= 60; + + if(h < 0.0){ h = h + 360;} + if(h > 359.99){ h = 359.99;} + return isnan(h) ? 0.0 : h; +} + + +rgbpixel_t HSVToRGB(float H) { + float r = 0.0, g = 0.0, b = 0.0; + float S = 1.0; + float V = 1.0; + + if (H < 0) { H += 360; } + if (H > 359) { H -= 360; } + + if (S == 0.0) { + r = V; g = V; b = V; + }else { + int i; + float f, p, q, t; + + if (H == 360){ H = 0; } + else { H = H / 60; } + + i = (int)trunc(H); + f = H - i; + + p = V * (1.0 - S); + q = V * (1.0 - (S * f)); + t = V * (1.0 - (S * (1.0 - f))); + + switch (i) { + case 0: + r = V; g = t; b = p; break; + case 1: + r = q; g = V; b = p; break; + case 2: + r = p; g = V; b = t; break; + case 3: + r = p; g = q; b = V; break; + case 4: + r = t; g = p; b = V; break; + default: + r = V; g = p; b = q; break; + } + } + + rgbpixel_t pix; + pix.red = (uint8_t)(r * 255); + pix.grn = (uint8_t)(g * 255); + pix.blu = (uint8_t)(b * 255); + + return pix; +} + diff --git a/z_old/HSVTable.h b/z_old/HSVTable.h new file mode 100644 index 0000000..f88957c --- /dev/null +++ b/z_old/HSVTable.h @@ -0,0 +1,23 @@ +#ifndef HSVTABLE_H +#define HSVTABLE_H + +#include "LEDStrip.h" + + +rgbpixel_t trueHSV(int angle); + +rgbpixel_t powerHSV(int angle); + +rgbpixel_t sineHSV(int angle); + +rgbpixel_t HSVToRGB(float H); + +// Returne a 0-360 float +float RGBToHSV(rgbpixel_t rgb); + +// Input 0-360 float + + + + +#endif \ No newline at end of file diff --git a/z_old/LEDStrip.cpp b/z_old/LEDStrip.cpp new file mode 100644 index 0000000..dd54478 --- /dev/null +++ b/z_old/LEDStrip.cpp @@ -0,0 +1,473 @@ +#include "LEDStrip.h" +#include "driver/i2s.h" +#include +#include "HSVTable.h" + +static const char* tag = "LEDStrip"; + +LEDSTRIP::LEDSTRIP(int port, int size, int pin, LED_ORDER ledOrder, int shift, int offset) +{ + this->port = port; + this->size = size; + this->shift = shift; + this->offset = offset; + this->effSize = this->size - this->offset; + this->ledOrder = ledOrder; + + // create pixel array and buffer array + pixels = new rgbpixel_t[size]; + out_buffer_size = size * PIXEL_SIZE; + out_buffer = new uint8_t[out_buffer_size + RESET_BUFFER_SIZE]; + reset_buffer = &out_buffer[out_buffer_size + 1]; // pointer to reset buffer section + active_out_buffer = &out_buffer[0] + (offset * PIXEL_SIZE); // start of the first active + + // initialize buffer + memset(pixels, 0, this->size * sizeof(rgbpixel_t)); + memset(out_buffer, 0, out_buffer_size + RESET_BUFFER_SIZE); + + dma_frame_size = I2S_TX_DMA_BUFF_SIZE; + if((size * PIXEL_SIZE) < I2S_TX_DMA_BUFF_SIZE) { + dma_frame_size = size * PIXEL_SIZE; + } + + // Although the buffer can be smaller than total buffer size it will require more cpu interrupts + // So this provides for the entire size rounded up to the closes buffer chunk (I2S_TX_DMA_BUFF_SIZE) + int dma_buff_count = 1 + ((out_buffer_size + RESET_BUFFER_SIZE + 2) / I2S_TX_DMA_BUFF_SIZE); + + // install driver + i2s_driver_config_t i2s_config = { + .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_TX), + .sample_rate = SAMPLE_RATE, + .bits_per_sample = I2S_BITS_PER_SAMPLE_8BIT, + .channel_format = i2s_channel_fmt_t(I2S_CHANNEL_FMT_RIGHT_LEFT), + .communication_format = (i2s_comm_format_t)(I2S_COMM_FORMAT_STAND_I2S), + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL2, // high interrupt priority, + //.intr_alloc_flags = ESP_INTR_FLAG_LOWMED, // high interrupt priority, + .dma_buf_count = dma_buff_count, // + .dma_buf_len = I2S_TX_DMA_BUFF_SIZE, + .use_apll = true + }; // possibly needed for accuracy + + //i2sEventQueue = xQueueCreate(8, sizeof(i2s_event_t)); + esp_err_t err = i2s_driver_install((i2s_port_t)port, &i2s_config, 8, &i2sEventQueue); + + if(err){ + if(err == ESP_ERR_INVALID_ARG) {ESP_LOGE(tag, "I2S driver error: Parameter error");} + else if(err == ESP_ERR_NO_MEM) {ESP_LOGE(tag, "I2S driver error: Out of Memory");} + else if(err == ESP_ERR_INVALID_STATE) {ESP_LOGE(tag, "I2S driver error: Port is in use");} + } + else{ + i2s_pin_config_t pin_config = {.bck_io_num = I2S_PIN_NO_CHANGE, + .ws_io_num = I2S_PIN_NO_CHANGE, + .data_out_num = pin, + .data_in_num = I2S_PIN_NO_CHANGE }; + + ESP_LOGV(tag, "I2S port pin configured...."); + err = i2s_set_pin((i2s_port_t)port, &pin_config); + if(err){ + if(err == ESP_ERR_INVALID_ARG) {ESP_LOGE(tag, "I2S driver error: Parameter error");} + else if(err == ESP_FAIL) {ESP_LOGE(tag, "I2S driver error: IO error");} + } + else{ + ESP_LOGV(tag, "I2S pin config success!"); + } + } + +} + + +#if(HIGH_RES_BIT_RATE == 1) +static const uint8_t bitpatterns[2] = {0b11000000, 0b11111100}; +void LEDSTRIP::updateBuff(void) { + uint8_t* buff = active_out_buffer; + for (uint16_t i = 0; i < this->effSize; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 7 & 0x01]; + *buff++ = bitpatterns[red >> 6 & 0x01]; + *buff++ = bitpatterns[red >> 5 & 0x01]; + *buff++ = bitpatterns[red >> 4 & 0x01]; + *buff++ = bitpatterns[red >> 3 & 0x01]; + *buff++ = bitpatterns[red >> 2 & 0x01]; + *buff++ = bitpatterns[red >> 1 & 0x01]; + *buff++ = bitpatterns[red & 0x01]; + + *buff++ = bitpatterns[grn >> 7 & 0x01]; + *buff++ = bitpatterns[grn >> 6 & 0x01]; + *buff++ = bitpatterns[grn >> 5 & 0x01]; + *buff++ = bitpatterns[grn >> 4 & 0x01]; + *buff++ = bitpatterns[grn >> 3 & 0x01]; + *buff++ = bitpatterns[grn >> 2 & 0x01]; + *buff++ = bitpatterns[grn >> 1 & 0x01]; + *buff++ = bitpatterns[grn & 0x01]; + + *buff++ = bitpatterns[blu >> 7 & 0x01]; + *buff++ = bitpatterns[blu >> 6 & 0x01]; + *buff++ = bitpatterns[blu >> 5 & 0x01]; + *buff++ = bitpatterns[blu >> 4 & 0x01]; + *buff++ = bitpatterns[blu >> 3 & 0x01]; + *buff++ = bitpatterns[blu >> 2 & 0x01]; + *buff++ = bitpatterns[blu >> 1 & 0x01]; + *buff++ = bitpatterns[blu & 0x01]; + } + + *buff++ = 0; +} +#else +static const uint16_t bitpatterns[4] = {0x88, 0x8e, 0xe8, 0xee}; +void LEDSTRIP::updateBuff(void) +{ + uint8_t* buff = active_out_buffer; + + switch(this->ledOrder){ + case ORDER_RGB: + for (uint16_t i = 0; i < this->effSize; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + } + break; + case ORDER_RBG: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + } + break; + case ORDER_GRB: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + } + break; + case ORDER_GBR: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + } + break; + case ORDER_BRG: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + } + break; + default: //ORDER_BGR: + for (uint16_t i = 0; i < this->size; i++) + { + uint8_t red = pixels[i].red >> powerDiv; + uint8_t grn = pixels[i].grn >> powerDiv; + uint8_t blu = pixels[i].blu >> powerDiv; + + *buff++ = bitpatterns[blu >> 6 & 0x03]; + *buff++ = bitpatterns[blu >> 4 & 0x03]; + *buff++ = bitpatterns[blu >> 2 & 0x03]; + *buff++ = bitpatterns[blu & 0x03]; + + *buff++ = bitpatterns[grn >> 6 & 0x03]; + *buff++ = bitpatterns[grn >> 4 & 0x03]; + *buff++ = bitpatterns[grn >> 2 & 0x03]; + *buff++ = bitpatterns[grn & 0x03]; + + *buff++ = bitpatterns[red >> 6 & 0x03]; + *buff++ = bitpatterns[red >> 4 & 0x03]; + *buff++ = bitpatterns[red >> 2 & 0x03]; + *buff++ = bitpatterns[red & 0x03]; + } + break; + } +} +#endif + +void LEDSTRIP::show(bool update) +{ + if(update){ updateBuff(); }; + + //i2s_stop((i2s_port_t)this->port); + i2s_start((i2s_port_t)this->port); + i2s_zero_dma_buffer((i2s_port_t)this->port); + i2s_write_data(out_buffer, out_buffer_size + RESET_BUFFER_SIZE); + + /* + i2s_event_t i2s_event; + while(1){ + if(xQueueReceive(i2sEventQueue, &i2s_event, 100)){ + if(i2s_event.type == I2S_EVENT_TX_DONE) { break;} + }else{ + break; + } + } + */ +} + +void LEDSTRIP::setPowerDiv(uint8_t div){ + if(div < 0){div = 0;} + if(div > 4){div = 4;} + powerDiv = div; +} + +// Function to write data to the I2S buffer +void LEDSTRIP::i2s_write_data(const void *data, size_t len) +{ + size_t bytesWritten = 0; + + while (bytesWritten < len) { + size_t bytesToWrite = len - bytesWritten; + + size_t written = 0; + esp_err_t err = i2s_write(I2S_NUM_0, (const char*)data + bytesWritten, bytesToWrite, &written, portMAX_DELAY); + if (err != ESP_OK) { + // Handle error + ESP_LOGE(tag, "i2s send error: %d", err); + break; + } + + bytesWritten += written; + } +} + +inline int LEDSTRIP::calcIndex(int index) +{ + //int x = (index + shift) % effSize; + //if(x < 0) { // convert nex index to positive range + // x += effSize; + //} + //return x + offset; + int x = (index + shift) % effSize; + x = (x < 0) ? (x + effSize) : x; + return (x + offset) % effSize; +} + +void LEDSTRIP::setPixel(int index, rgbpixel_t& col) +{ + pixels[calcIndex(index)] = col; +} + +void LEDSTRIP::setPixel(int index, const rgbpixel_t col) +{ + pixels[calcIndex(index)] = col; +} + +/* +void LEDSTRIP::setPixel(int index, rgbpixel_t col, uint8_t scale) +{ + uint16_t n = scale + 1; + uint8_t r = (col.red * n) >> 8; + uint8_t g = (col.grn * n) >> 8; + uint8_t b = (col.blu * n) >> 8; + pixels[calcIndex(index)] = {r, g, b}; +} +*/ + +void LEDSTRIP::setPixelRaw(int index, rgbpixel_t& col){ + pixels[index] = col; +} + +void LEDSTRIP::setPixelMirrored(int index, rgbpixel_t col) +{ + pixels[calcIndex(index)] = col; + pixels[calcIndex(-index-1)] = col; +} + +rgbpixel_t LEDSTRIP::getPixel(int index) +{ + return pixels[calcIndex(index)]; +} + +void LEDSTRIP::rotatePixels(LED_DIR dir) +{ + // store the shifted out pixels + rgbpixel_t tPix; + if(dir == DIR_FWD){ + tPix = pixels[size -1]; + for(int i=size-1; i > offset; i--){ + pixels[i] = pixels[i-1]; + } + pixels[offset] = tPix; + }else{ + tPix = pixels[offset]; + for(int i=offset; i < size-1; i++){ + pixels[i] = pixels[i+1]; + } + pixels[size-1] = tPix; + } +} + +void LEDSTRIP::shiftPixels(int numPixels, LED_DIR dir) +{ + // Calculate the shift direction and absolute shift count + int shiftDir = (dir == DIR_FWD) ? 1 : -1; + int absShiftCount = (numPixels % this->effSize) * shiftDir; + + // Shift the pixels by swapping them in place using circular buffering + for (int i = 0; i < this->effSize; i++) { + rgbpixel_t temp = pixels[i]; + int j = i; + + while (true) { + int k = j + absShiftCount; + + if (k < 0) { + k += this->effSize; + } else if (k >= this->effSize) { + k -= this->effSize; + } + + if (k == i) { + break; + } + + pixels[j] = pixels[k]; + j = k; + } + + pixels[j] = temp; + + // Check if we have completed a cycle + if (j == i + absShiftCount) { + break; + } + } +} + +void scalePixel(rgbpixel_t& pix, uint8_t _scale) +{ + uint16_t n = _scale + 1; + pix.red = (pix.red * n) >> 8; + pix.grn = (pix.grn * n) >> 8; + pix.blu = (pix.blu * n) >> 8; +} + +void linearizePixel(rgbpixel_t& pix){ + pix.red = ((int)pix.red * (int)pix.red ) >> 8; + pix.grn = ((int)pix.grn * (int)pix.grn ) >> 8; + pix.blu = ((int)pix.blu * (int)pix.blu ) >> 8; +} + + +void PixelFadeToBlack(rgbpixel_t& pix, uint8_t fadeValue) { + pix.red = (pix.red <= 8) ? 0 : pix.red - ((pix.red * fadeValue)>>8); + pix.grn = (pix.grn <= 8) ? 0 : pix.grn - ((pix.grn * fadeValue)>>8); + pix.blu = (pix.blu <= 8) ? 0 : pix.blu - ((pix.blu * fadeValue)>>8); +} + +void LEDSTRIP::zeroPixels(void) +{ + for(int i=0; i < this->size; i++){ + pixels[i] = {0,0,0}; + } +} + +void LEDSTRIP::fill(rgbpixel_t color, int startIndex, int count) +{ + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) { + pixels[calcIndex(i)] = color; + } +} + +// a factor of 0=no changer, factor=255 newCol replaces 100% +void LEDSTRIP::transitionPixel(int index, rgbpixel_t& newCol, uint8_t factor) +{ + int i = calcIndex(index); + int f = factor + 1; + pixels[i].red = (pixels[i].red * (256 - f) + newCol.red * f) >> 8; + pixels[i].grn = (pixels[i].grn * (256 - f) + newCol.grn * f) >> 8; + pixels[i].blu = (pixels[i].blu * (256 - f) + newCol.blu * f) >> 8; +} + +LED_ORDER getRGBOrder(const char* order){ + if(strcmp(order, "rgb")==0){ return ORDER_RGB; } + if(strcmp(order, "rbg")==0){ return ORDER_RBG; } + if(strcmp(order, "grb")==0){ return ORDER_GRB; } + if(strcmp(order, "gbr")==0){ return ORDER_GBR; } + if(strcmp(order, "brg")==0){ return ORDER_BRG; } + if(strcmp(order, "bgr")==0){ return ORDER_BGR; } + else{ return ORDER_GRB; } +} \ No newline at end of file diff --git a/z_old/LEDStrip.h b/z_old/LEDStrip.h new file mode 100644 index 0000000..6c18f0a --- /dev/null +++ b/z_old/LEDStrip.h @@ -0,0 +1,117 @@ +#ifndef LEDSTRIP_H +#define LEDSTRIP_H + +//#include +#include "LEDStrip.h" +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "esp_intr_alloc.h" + +#define HIGH_RES_BIT_RATE 0 + +#if(HIGH_RES_BIT_RATE == 1) + #define PIXEL_SIZE (8*3) + #define SAMPLE_RATE (360000) // 1.6us cycle + #define RESET_BUFFER_SIZE (100 * PIXEL_SIZE) // led latching timeout 300us mininum (50us older chips) +#else + // This can be set to 1 (normally 50) as long at the update freq isn't fast enough to interfere with the required reset length time + // This reduces memory requirements + #define RESET_PIXELS 1 + #define PIXEL_SIZE (4*3)// each colour takes 4 bytes and 3 colors per pixel + #define SAMPLE_RATE (160000) // 1.6us cycle + #define RESET_BUFFER_SIZE (RESET_PIXELS * PIXEL_SIZE) // led latching timeout 4 or more pixel lengths +#endif + +#define I2S_TX_DMA_BUFF_SIZE 128 // smaller buffer means it will start quicker + +typedef struct{ + uint8_t red; + uint8_t grn; + uint8_t blu; +} rgbpixel_t; + +enum LED_DIR{DIR_REV, DIR_FWD}; + +enum LED_ORDER{ORDER_RGB, ORDER_RBG, ORDER_GRB, ORDER_GBR, ORDER_BRG, ORDER_BGR}; + +LED_ORDER getRGBOrder(const char* order); + +void PixelFadeToBlack(rgbpixel_t& pix, uint8_t fadeValue); + +void scalePixel(rgbpixel_t& pix, uint8_t _scale); +void linearizePixel(rgbpixel_t& pix); +//rgbpixel_t scalePixel(rgbpixel_t pix, uint8_t _scale); + +//void i2sDmaInterruptHandler(void *arg); + +class LEDSTRIP { + public: + SemaphoreHandle_t i2sSemaphore; + int effSize; + int size; + int shift; + int offset; + int powerDiv = 0; + LED_ORDER ledOrder; + rgbpixel_t* pixels; + QueueHandle_t i2sEventQueue; + + LEDSTRIP(int port, int size, int pin, LED_ORDER ledOrder, int shift=0, int offset=0); + // process buffer before sending + void updateBuff(void); + // Send data to dma and I2S port + void show(bool update=true); + + void setPixel(int index, rgbpixel_t& col); + void setPixel(int index, const rgbpixel_t col); + void setPixel(int index, rgbpixel_t& col, uint8_t scale=255); + //void setPixelTrueHue(int index, int hueAngle); + //void setPixelPowerHue(int index, int hueAngle); + //void setPixelSineHue(int index, int hueAngle); + void setPixelRaw(int index, rgbpixel_t& col); + + void setPixelMirrored(int index, rgbpixel_t col); + rgbpixel_t getPixel(int index); + + //void scale(rgbpixel_t& pix, uint8_t _scale); + void fill(rgbpixel_t color, int startIndex, int count); + void fade(rgbpixel_t& col); + void zeroPixels(void); + //void setMirrored(void); + + // mirror pixels about the shift position + void rotatePixels(LED_DIR dir); + void shiftPixels(int numPixels, LED_DIR dir); + void transitionPixel(int index, rgbpixel_t& newCol, uint8_t factor); + //rgbpixel_t hueToRGB(uint8_t hue, uint8_t sat, uint8_t val); + + void setPowerDiv(uint8_t div); + + + private: + static int instanceCount; //number in class instances + + int port; + + + //intr_handle_t i2sInterruptHandle; + + int dma_frame_size; + // dma output buffer + uint8_t* out_buffer; + int out_buffer_size; + // pointer to actual active part of buffer + uint8_t* active_out_buffer; + // index position calculation + uint8_t* reset_buffer; + void i2s_write_data(const void *data, size_t size); + int calcIndex(int ind); + +}; + +//void IRAM_ATTR i2s0DmaTransmitComplete(void* arg); +//void IRAM_ATTR i2s1DmaTransmitComplete(void* arg); + +#endif diff --git a/z_old/color_tools.cpp b/z_old/color_tools.cpp new file mode 100644 index 0000000..3dc53d0 --- /dev/null +++ b/z_old/color_tools.cpp @@ -0,0 +1,69 @@ +#include "color_tools.h" +#include + +#define MAX_HUE 360.0 + + +/************************* CLASS - HUE PALLET DISPENSER ************************/ + +HUE_PALLET_DISPENSER::HUE_PALLET_DISPENSER(void){ + Initialize(0, 360, 6); +} + +void HUE_PALLET_DISPENSER::Initialize(float hue, float range, int colSteps){ + this->range = range; + this->hueSteps = colSteps; + + this->startHue = hue - this->range / 2; + if(this->startHue < 0.0){ + this->startHue += MAX_HUE; + }else if(this->startHue > MAX_HUE){ + this->startHue -= MAX_HUE; + } + + this->hueIndex = 0; + + if(this->hueSteps <= 1){ + this->hueIncrement = 0.0; + this->currHue = hue; + }else{ + this->hueIncrement = this->range / (this->hueSteps - 1); + } +} + + +int HUE_PALLET_DISPENSER::GetNextPalletHue(void){ + if(this->hueSteps > 1){ + this->currHue = this->startHue + this->hueIndex * this->hueIncrement; + if(this->currHue < 0){ + this->currHue += MAX_HUE; + }else if(this->currHue > MAX_HUE){ + this->currHue -= MAX_HUE; + } + + // TODO Remove later + //rgbpixel_t p = HUEtoRGB(huePallet.currHue); + //Log.traceln(" index: %d, hue= %F, col: %d, %d, %d", huePallet.hueIndex, huePallet.currHue, p.red, p.grn, p.blu); + + this->hueIndex = ++this->hueIndex % this->hueSteps; + return round(this->currHue); + }else{ + return round(this->currHue); + } +} + +float HUE_PALLET_DISPENSER::PeekNextPalletHue(int hueOffset){ + float tempHue = this->startHue + (this->hueIndex + hueOffset) * this->hueIncrement; + + if(tempHue < 0){ + tempHue += MAX_HUE; + }else if(tempHue > MAX_HUE){ + tempHue -= MAX_HUE; + } + + return tempHue; +} + +void HUE_PALLET_DISPENSER::SetHueIndex(int hueIndex){ + this->currHue = hueIndex; +} diff --git a/z_old/color_tools.h b/z_old/color_tools.h new file mode 100644 index 0000000..176ef2c --- /dev/null +++ b/z_old/color_tools.h @@ -0,0 +1,38 @@ +#ifndef _COLOR_TOOLS_H +#define _COLOR_TOOLS_H + + +class HUE_PALLET_DISPENSER { + public: + float range = 1.0; + float hueIncrement = 1.0; + float currHue = 1.0; + int hueIndex = 0; + int hueSteps = 1; + float startHue = 0; + + //HUE_PALLET_DISPENSER(float hue1, float hue2, int colSteps); + HUE_PALLET_DISPENSER(void); + void Initialize(float hue, float range, int colSteps); + int GetNextPalletHue(void); + float PeekNextPalletHue(int offset); + void SetHueIndex(int hueIndex); + private: +}; + +enum SPEED_PROFILE {SPEED_SIMPLE, SPEED_SQUARE, SPEED_LIN_ACCEL}; + +class SPEED_DISPENSER{ + public: + + SPEED_DISPENSER(void); + + void Initialize(int min, int max, int steps, SPEED_PROFILE prof); + int GetNextSpeedInterval(void); +}; + + + + + +#endif \ No newline at end of file diff --git a/z_old/fileSystem.cpp b/z_old/fileSystem.cpp new file mode 100644 index 0000000..d36929d --- /dev/null +++ b/z_old/fileSystem.cpp @@ -0,0 +1,74 @@ +#include "fileSystem.h" +#include +#include + +static const char* tag = "fs"; + +void Init_File_System(void) +{ + if(!LittleFS.begin()){ + ESP_LOGE(tag, "An Error occured while mounting LittleFS"); + return; + } + + // To format all space in LITTLEFS + // LITTLEFS.format() + + // Get all information of your LITTLEFS + ESP_LOGD(tag, "File system info."); + ESP_LOGD(tag, "Total: %d, Used: %d, Free:%d", LittleFS.totalBytes(), LittleFS.usedBytes(), LittleFS.totalBytes() - LittleFS.usedBytes()); + + // Open dir folder + File dir = LittleFS.open("/"); + + // Cycle all the content + printAllFiles(); +} + +void getAllDirectories(String directoryList[], int& count) +{ + File root = LittleFS.open("/"); + if (!root.isDirectory()) { return; } // Root is not a directory + + directoryList[0] = "/"; + count = 1; + + File file = root.openNextFile(); + while (file) { + if (file.isDirectory()) { + directoryList[count] = '/' + String(file.name()); + count++; + } + file = root.openNextFile(); + } + + root.close(); +} + +void printFilesInDirectories(String directoryList[], int count) +{ + for (int i = 0; i < count; i++) { + ESP_LOGD(tag, "Dir: %s", directoryList[i].c_str()); + + File dir = LittleFS.open(directoryList[i]); + File file = dir.openNextFile(); + while (file) { + if (!file.isDirectory()) { + ESP_LOGD(tag, " File: %s", file.name()); + } + file = dir.openNextFile(); + } + dir.close(); + } +} + +void printAllFiles(void) +{ + String directories[8]; + int dirCount = 0; + + getAllDirectories(directories, dirCount); + + ESP_LOGD(tag, "File System Listing:"); + printFilesInDirectories(directories, dirCount); +} \ No newline at end of file diff --git a/z_old/fileSystem.h b/z_old/fileSystem.h new file mode 100644 index 0000000..1dbb701 --- /dev/null +++ b/z_old/fileSystem.h @@ -0,0 +1,23 @@ +#ifndef _FILESYSTEM_H +#define _FILESYSTEM_H + +#define SPIFFS LITTLEFS +#include + +const int MAX_DIRECTORIES = 8; + +void Init_File_System(void); + +void readTestFile(void); + +//void printDirectory(File dir, int numTabs = 3); + +void writeFilesToSerial(void); + +void getAllDirectories(String directoryList[], int& count); + +void printFilesInDirectories(String directoryList[], int count); + +void printAllFiles(void); + +#endif \ No newline at end of file diff --git a/z_old/my_board.cpp b/z_old/my_board.cpp new file mode 100644 index 0000000..8eeeb64 --- /dev/null +++ b/z_old/my_board.cpp @@ -0,0 +1,249 @@ +#include "my_board.h" + +#include +#include +#include +#include "global.h" +#include "JsonConstrain.h" + +static const char* tag = "board"; +PWM_Output *pwmOut[4]; +RAMP_Output *RearLightControl; + +void Init_Board_Pins(void) +{ + pinMode(BoardLED1, OUTPUT); + pinMode(BoardLED2, OUTPUT); + + pinMode(Button1_Pin, INPUT_PULLUP); + pinMode(Button2_Pin, INPUT_PULLUP); + pinMode(Button3_Pin, INPUT_PULLUP); + + ESP_LOGV(tag, "Board pins initialized..."); +} + +void Init_PWM_Outputs(void) +{ + File file = LittleFS.open("/cfg/relays.json"); + if(!file){ + ESP_LOGE(tag, "Error opening relays.json..."); + }else{ + StaticJsonDocument<1024> doc; + DeserializationError error = deserializeJson(doc, file); + if(!error){ + file.close(); + if(error){ ESP_LOGE(tag, "relays.json deserialize error!.."); return;} + + JsonArray jsArray = doc["relays"]; + + if(jsArray.isNull() || jsArray.size() < 1 || jsArray.size() > 4 ) { return; } + + int x = 0; + for(JsonObject obj : jsArray){ + //Default config if keys are not found + if (!obj.containsKey("pin") || !obj.containsKey("freq") || !obj.containsKey("max") || !obj.containsKey("default")) { + pwmOut[x] = new PWM_Output(0, x, 12, 500, 100.0, false); + ESP_LOGD(tag, "pwmOut[%d] default config", x); + x++; + continue; + } + + int pin; + switch(x){ + case 0: + pin = jsonConstrainInt(obj, "pin", 0, 48, RELAY1_Pin); + break; + case 1: + pin = jsonConstrainInt(obj, "pin", 0, 48, RELAY2_Pin); + break; + case 2: + pin = jsonConstrainInt(obj, "pin", 0, 48, RELAY3_Pin); + break; + default: + pin = jsonConstrainInt(obj, "pin", 0, 48, RELAY4_Pin); + } + + int freq = jsonConstrainInt(obj, "freq", 100, 2000, 500); + float maxVal = jsonConstrainInt(obj, "max", 2.0, 100.0, 95); + float defaultVal = jsonConstrainInt(obj, "default", 0.0, 100.0, 40); + ESP_LOGD(tag, "pwmOut[%d]: freq:%d, max:%F, default:%F", x, freq, maxVal, defaultVal); + + pwmOut[x] = new PWM_Output(pin, x, RELAY_RES, freq, maxVal, false); + pwmOut[x]->setOutput(defaultVal); + x++; + } + }else{ + ESP_LOGE(tag, "Deserialization error on relays.json"); + } + } +} + +int linearizeLED(float inp){ + if(inp > 100.0){ inp = 100.0;}; + return (inp*inp*0.4095f); +} + +/******************* PWM_Output Definition ********************/ + +// max: 0-100% +PWM_Output::PWM_Output(uint8_t pin, uint8_t ch, int res, uint32_t freq, float maxDuty, bool visionCorrected) +{ + this->currDuty = 0; + this->ch = ch; + this->maxDuty = maxDuty; + this->visionCorrected = visionCorrected; + this->freq = freq; + this->res = res; + //this->msecRampRate = msecRampRate; + pinMode(pin, OUTPUT); + if(!ledcSetup(ch, freq, res)) { + ESP_LOGE(tag, "pwmOut-> ch:%d ledcSetup failed!", ch); + return; + } + ledcAttachPin(pin, ch); + setOutput(this->currDuty); +} + +// Range is 0 to 100% +void PWM_Output::setOutput(float duty) +{ + if(duty > maxDuty) { duty = maxDuty;} + + // calculate correct duty value + int outDutyVal; + if(this->visionCorrected){ outDutyVal = linearizeLED(duty); } + else{ outDutyVal = duty * 40.95f; } + + // FIXME pwm output ramp cause a freeze here + // Smooth Transition to final duty value + /* + if(msecRampRate){ + int d = this->currOutVal; + float dutyInc = (outDutyVal - this->currOutVal)/msecRampRate; + for(;;){ + d += dutyInc; + if(d < 0){d = 0;} + if(d > 4095){d = 4095;} + ledcWrite(this->ch, d); + if(d >= outDutyVal || d <= 0){ break; } + vTaskDelay(msecRampRate); + } + ledcWrite(this->ch, outDutyVal); + this->currOutVal = outDutyVal; + } + else{ + */ + ledcWrite(this->ch, outDutyVal); + this->currOutVal = outDutyVal; + //} + + this->currDuty = duty; +} + +void PWM_Output::setFreq(uint32_t fq) +{ + uint32_t newFreq; + + if(this->freq != fq){ + newFreq = ledcChangeFrequency(this->ch, fq, this->res); + if(newFreq){ + this->freq = fq; + } + } +} + + + +/******************* RAMP_Output Definition ********************/ + + + +void Initialize_Rear_Control(int relayIndex, int buttonIndex, int rampTime, int steps, float min, float max){ + RearLightControl = new RAMP_Output(pwmOut[relayIndex], btn[buttonIndex], rampTime, steps, min, max); +} + +RAMP_Output *RAMP_Output::instance = nullptr; // Initialize the static member variable + +void longPressStartCallback(void){ + xTimerStart(RearLightControl->timerHandle, 0); +} + +void longPressStopCallback(void){ + xTimerStop(RearLightControl->timerHandle, 0); + RAMP_Output::instance->SwitchDirection(); // Call the non-static member function using the stored instance +} + +void SingleClickCallback(void){ + RAMP_Output::instance->ClickOnOff(); +} + +void RAMP_Output::TimerCallback(TimerHandle_t xTimer) { + RearLightControl->IncrementTick(); +} + +RAMP_Output::RAMP_Output(PWM_Output *output, OneButton *button, int rampTime, int steps, float min, float max) { + this->Output = output; + this->Button = button; + this->rampTime = rampTime; + this->steps = steps; + this->min = min; + this->max = max; + this->stepSize = (max - min) / steps; + + ESP_LOGD(tag, "Rear Lights: stepSize= %.2f", stepSize); + + instance = this; // Store the instance in the static member variable + + if (this->Button) { + this->Button->attachLongPressStart(longPressStartCallback); + this->Button->attachLongPressStop(longPressStopCallback); + this->Button->attachClick(SingleClickCallback); + } else { + Serial.println("Error: Button is nullptr"); + } + + Output->visionCorrected = true; + Output->currDuty = min; + Output->setOutput(min); + + timerHandle = xTimerCreate("RampTimer", pdMS_TO_TICKS(rampTime/8), pdTRUE, this, TimerCallback); +} + +void RAMP_Output::ClickOnOff(void) { + if(IsOn){ + IsOn = false; + Output->setOutput(0); + }else{ + IsOn = true; + Output->setOutput(lastDuty); + } +} + +void RAMP_Output::IncrementTick(void) { + if (dirFwd) { + // Increment output value while ensuring it doesn't exceed max + float newValue = std::min(Output->currDuty + stepSize, max); + Output->setOutput( newValue); + lastDuty = newValue; + //ESP_LOGD(tag, "up: %.2f", newValue); + } else { + // Decrement output value while ensuring it doesn't go below min + float newValue = std::max(Output->currDuty - stepSize, min); + Output->setOutput( newValue ); + lastDuty = newValue; + //ESP_LOGD(tag, "down: %.2f", newValue); + } +} + +void RAMP_Output::SwitchDirection(void) { + dirFwd = !dirFwd; +} + +// Add destructor to clean up resources +RAMP_Output::~RAMP_Output() { + xTimerDelete(timerHandle, 0); + // Add any other necessary cleanup here +} + + + diff --git a/z_old/my_board.h b/z_old/my_board.h new file mode 100644 index 0000000..27a93fb --- /dev/null +++ b/z_old/my_board.h @@ -0,0 +1,162 @@ +#ifndef _MY_BOARD_H +#define _MY_BOARD_H + +#include +#include "my_buttons.h" + + +#define S3MODULEKIT 0 + +//************************* + +#define BoardLED1 17 +#define BoardLED2 18 + +#define Set_Status_LED1(x) digitalWrite(BoardLED1, x) +#define Set_Status_LED2(x) digitalWrite(BoardLED2, x) + +//************************ + +#define Button1_Pin 8 +#define Button2_Pin 19 +#define Button3_Pin 0 + +#define Button1_State digitalRead(Button1_Pin) +#define Button2_State digitalRead(Button2_Pin) +#define Button3_State digitalRead(Button3_Pin) + +//*********************** + +#define I2C_SDA1_Pin 1 +#define I2C_SCL1_Pin 2 + +#define Buzzer_Pin 37 + +#if S3MODULEKIT == 1 + #define RGBLED1_Pin 48 +#else + #define RGBLED1_Pin 3 +#endif + #define RGBLED2_Pin 46 + +#define RX433_Pin 16 +#define TX433_Pin 38 + +#define VIN_12V_Pin 20 + +#define RELAY_RES 12 +#define RELAY1_Pin 45 +#define RELAY2_Pin 48 +#define RELAY3_Pin 47 +#define RELAY4_Pin 21 +#define RELAY5_Pin 35 /* test*/ + +#define FanIndex 3 + +#define TMP102_ADDR 72 + +#define buzzerCh 4 /* 0-7chs available, 0-3 used by pwm outputs */ + +#define TOUCH1_Pin 9 +#define TOUCH2_Pin 10 +#define TOUCH3_Pin 11 +#define TOUCH4_Pin 12 +#define TOUCH5_Pin 13 +#define TOUCH_SHIELD_Pin 9 + +#define EXT1_Pin 35 +#define EXT2_Pin 36 + +#define OLED_DC 7 +#define OLED_RST 6 +#define OLED_MOSI 5 +#define OLED_SCK 4 +#define OLED_CS 15 + + +int linearizeLED(float inp); + +#define SetRelay(c,val) ledcWrite(c, val); +#define SetFrontLightCorr(val) ledcWrite(FrontConstLightCh,linearizeLED(val)) +#define SetRearLightCorr(val) ledcWrite(RearConstLightCh,linearizeLED(val)) +#define SetFrontLight(val) ledcWrite(FrontConstLightCh, val) +#define SetRearLight(val) ledcWrite(RearConstLightCh,val) + +void Init_Board_Pins(void); + +void Init_PWM_Outputs(void); + +struct _relay +{ + int freq; + bool linCorr; + int max; + int min; +}; + +struct PWMOUT{ + float max; + bool visionCorrected; + int resolution; + int frequency; +}; + +class PWM_Output { + public: + float currDuty; + bool visionCorrected; + PWM_Output(uint8_t pin, uint8_t ch, int res, uint32_t freq, float maxDuty, bool visionCorrected=false); + void setOutput(float duty); + void setFreq(uint32_t fq); + + float getMaxDuty() const { + return maxDuty; + } + void setMaxDuty(float duty) { + // add any validation or constraints here + if(duty < 0) {duty = 0.0;} + else{if(duty > 100.0) {duty = 100.0;}} + maxDuty = duty; + } + + private: + uint8_t ch; + uint32_t freq; + uint8_t res; + uint8_t currOutVal; + float maxDuty; + int msecRampRate; + +}; + +extern PWM_Output *pwmOut[4]; + + +// Push button Ramp Up/Down Logic for Lights +class RAMP_Output { + public: + TimerHandle_t timerHandle; + RAMP_Output(PWM_Output *output, OneButton *button, int rampTime, int steps, float min, float max); + ~RAMP_Output(); + void IncrementTick(void); + void SwitchDirection(void); + void ClickOnOff(void); + static void TimerCallback(TimerHandle_t xTimer); + static RAMP_Output *instance; // Static member variable to store an instance + private: + PWM_Output *Output; + OneButton *Button; + int steps; + int rampTime; + float min; + float max; + float stepSize; + bool dirFwd = true; + float lastDuty; + bool IsOn = true; + +}; + +void Initialize_Rear_Control(int relayIndex, int buttonIndex, int rampTime, int steps, float min, float max); + +#endif diff --git a/z_old/my_buttons.cpp b/z_old/my_buttons.cpp new file mode 100644 index 0000000..d1465b3 --- /dev/null +++ b/z_old/my_buttons.cpp @@ -0,0 +1,92 @@ +#include "my_buttons.h" +#include "my_board.h" +#include "global.h" +#include "led_strip.h" +#include "my_buzzer.h" + +static const char* tag = "button"; + +OneButton *btn[3]; + +void Init_ButtonEvents(void) +{ + btn[0] = new OneButton(btn1Pin, true, true); + btn[1] = new OneButton(btn2Pin, true, true); + btn[2] = new OneButton(btn3Pin, true, true); + + if(btn[0] != NULL){ + btn[0]->setDebounceTicks(DEBOUCE_TIME); // just below the update period to guarantee 1 sample delay + btn[0]->attachClick(btn1_click); + btn[0]->attachDoubleClick(btn1_doubleClick); + } + else { + ESP_LOGW(tag, "Button1 Not Initialized"); + } + + if(btn[1] != NULL){ + btn[1]->setDebounceTicks(DEBOUCE_TIME); + btn[1]->attachClick(btn2_click); + btn[1]->attachDoubleClick(btn2_doubleClick); + btn[1]->attachLongPressStart(btn2_LongPressStart); + } + else { + ESP_LOGW(tag, "Button2 Not Initialized"); + } + + if(btn[2] != NULL){ + btn[2]->setDebounceTicks(DEBOUCE_TIME); + btn[2]->attachClick(btn3_click); + btn[2]->attachDoubleClick(btn3_doubleClick); + btn[2]->attachLongPressStart(btn3_LongPressStart); + btn[2]->attachLongPressStop(btn3_LongPressStop); + } + else { + ESP_LOGW(tag, "Button3 Not Initialized"); + } + + ESP_LOGV(tag, "Initialized Buttons Events...\n"); +} + +void btn1_click() { + IncrementEventIndex(); + Pulse_LED_Status(150); + ESP_LOGD(tag, "btn1 1x"); +} + +void btn1_doubleClick() { + ESP_LOGD(tag, "btn1 2x"); +} + +void btn2_click() { + Pulse_LED_Status(150); + Buzzer_Beep(150); + // send packet + ESP_LOGD(tag, "btn2 1x"); +} + +void btn2_doubleClick() { + ESP_LOGD(tag, "btn2 2x"); +} + +void btn2_LongPressStart(){ + // scan for devices +} + +void btn3_click() { + Pulse_LED_Status(150); + ESP_LOGD(tag, "btn3 1x"); +} + +void btn3_doubleClick() { + ESP_LOGD(tag, "btn3 2x"); +} + +//bool IncFwd = true; +void btn3_LongPressStart(){ + ESP_LOGD(tag, "btn3 long press"); +} + +void btn3_LongPressStop(){ + //IncFwd = !IncFwd; // switch increment direction +} + diff --git a/z_old/my_buttons.h b/z_old/my_buttons.h new file mode 100644 index 0000000..467eb12 --- /dev/null +++ b/z_old/my_buttons.h @@ -0,0 +1,34 @@ +#ifndef _MY_BUTTONS_H +#define _MY_BUTTONS_H + +#include "OneButton.h" +#include "global.h" + +#define DEBOUCE_TIME (BUTTON_UPDATE_PERIOD-24) + +#define btn1Pin Button1_Pin +#define btn2Pin Button2_Pin +#define btn3Pin Button3_Pin + +extern OneButton *btn[3]; + + +#define Update_Buttons() btn[1]->tick(); btn[2]->tick(); btn[3]->tick(); + +void Init_ButtonEvents(void); + +void btn1_click(); +void btn1_doubleClick(); + +void btn2_click(); +void btn2_doubleClick(); +void btn2_LongPressStart(); + +void btn3_click(); +void btn3_doubleClick(); +void btn3_LongPressStart(); +void btn3_LongPressStop(); + + + +#endif \ No newline at end of file diff --git a/z_old/my_buzzer.cpp b/z_old/my_buzzer.cpp new file mode 100644 index 0000000..2bd8dfc --- /dev/null +++ b/z_old/my_buzzer.cpp @@ -0,0 +1,123 @@ +#include "my_buzzer.h" +#include +#include +#include +#include +#include "my_board.h" +#include "global.h" +#include "JsonConstrain.h" +#include "global.h" + +//#define buzzPin Buzzer_Pin + +static const char* tag = "buzzer"; + +MelodyPlayer* player; + +BUZZ_TUNE buzzTune[12]; +uint8_t buzzPin; + +void Init_Buzzer(uint8_t pin) +{ + File file = LittleFS.open("/cfg/buzzer.json"); + if(!file){ + file.close(); + ESP_LOGE(tag, "Error opening buzzer.json..."); + }else{ + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + if(error){ ESP_LOGE(tag, "buzzer.json deserialize error!.."); return; } + + if(jsonConstrainBool(tag, doc.as(), "en", false)){ + //player = new MelodyPlayer(buzzPin, buzzerCh, false); + int freeChannel = findUnusedLedcChannel(); + freeChannel = 1; + player = new MelodyPlayer(buzzPin, freeChannel, false); + + if(player){ + Buzzer_Load_Tunes(doc.as()); // Load Tunes + ESP_LOGD(tag, "Buzzer initialized.. using Ch:%d", freeChannel); + } + else{ + ESP_LOGE(tag, "Buzzer initialization failed.."); + } + + } + } +} + +void Buzzer_Play_Tune(TUNE_TYPE tune, bool async, bool hasPriority) +{ + static int prev_tune = -1; + static Melody melody; + + //melody = MelodyFactory.loadRtttlString( buzzTune[tune].melody.c_str() ); + //player->playAsync(melody); + + + if(!player) { + ESP_LOGV(tag, "no buzzer"); + return; + } + + //if(!player->isPlaying() || (player->isPlaying() && hasPriority)){ + // ESP_LOGD(tag, "Playing tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str()); + + // if(prev_tune == tune){ + // ESP_LOGD(tag, "Same tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str()); + // for(int c = 0; c < buzzTune[tune].cycles; c++){ + // ( async ) ? player->playAsync() : player->play(); + // } + // } + // else{ + melody = MelodyFactory.loadRtttlString( buzzTune[tune].melody.c_str() ); + prev_tune = tune; + ESP_LOGD(tag, "New tune: %d, melody: %s", tune, buzzTune[tune].melody.c_str()); + + for(int c = 0; c < buzzTune[tune].cycles; c++){ + ( async ) ? player->playAsync(melody) : player->play(melody); + + } + // } + //} + //else{ + // ESP_LOGD(tag, "buzzer busy"); + //} + +} + +void Buzzer_Beep(int mSecs, int freq) +{ + ledcAttachPin(buzzPin, buzzerCh); + ledcSetup(buzzerCh, 2000, 8); + ledcWrite(buzzerCh, 125); + vTaskDelay(mSecs); + ledcWrite(buzzerCh, 0); +} + +void Buzzer_Load_Tunes(const JsonObject &doc) +{ + const char* tuneName[] PROGMEM = {"boot", "error", "success", "click", "beep", + "wifi-conn", "wifi-disc", "ble-conn", "ble-disc", + "download", "waiting", "restart", "test", "ack", nullptr}; + + int listCount = 0; + while (true){ + if(tuneName[listCount] == nullptr){ break; } + listCount++; + } + + JsonObject js[listCount]; // JsonObject leaks in a loop so create separate objects + + listCount = 3; + + for(int i = 0; i < listCount; i++){ + js[i] = doc[tuneName[i]]; + buzzTune[i].cycles = jsonConstrain(tag, js[i], "cycles", 1, 100, 1); + buzzTune[i].pause = jsonConstrain(tag, js[i], "pause", 0, 100, 0); + buzzTune[i].melody = jsonConstrainString(tag, js[i], "tune", "Ack:d=16,o=5,b=112:b,b#").c_str(); + ESP_LOGD(tag, "tune %d : %s", i, buzzTune[i].melody.c_str()); + } + ESP_LOGV(tag, "loaded %d tunes", listCount); +} \ No newline at end of file diff --git a/z_old/neo_colors.h b/z_old/neo_colors.h new file mode 100644 index 0000000..41e4cbd --- /dev/null +++ b/z_old/neo_colors.h @@ -0,0 +1,138 @@ +#ifndef NEO_COLORS_H +#define NEO_COLORS_H + +#include "LEDStrip.h" + +/* +rgbpixel_t pallet_rainbow[1]; +rgbpixel_t pallet_white[1]; +rgbpixel_t pallet_USA[1]; +rgbpixel_t pallet_halloween[1]; +rgbpixel_t pallet_christmas[1]; +rgbpixel_t pallet_autumn[1]; +rgbpixel_t pallet_summer[1]; +rgbpixel_t pallet_neon[1]; +*/ + + + +#define USE_CORRECTED_COLORS 0 + +#define col_black color_pallet[0] +#define col_white color_pallet[1] +#define col_red color_pallet[2] +#define col_green color_pallet[3] +#define col_blue color_pallet[4] +#define col_orange color_pallet[5] +#define col_yellow color_pallet[6] +#define col_cyan color_pallet[7] +#define col_magenta color_pallet[8] +#define col_purple color_pallet[9] +#define col_pink color_pallet[10] +#define col_teal color_pallet[11] +#define col_lime color_pallet[12] +#define col_indigo color_pallet[13] +#define col_maroon color_pallet[14] +#define col_navy color_pallet[15] +#define col_olive color_pallet[16] +#define col_beige color_pallet[17] +#define col_brown color_pallet[18] +#define col_coral color_pallet[19] +#define col_gold color_pallet[20] +#define col_gray color_pallet[21] +#define col_ivory color_pallet[22] +#define col_khaki color_pallet[23] +#define col_lavender color_pallet[24] +#define col_peach color_pallet[25] +#define col_periwinkle color_pallet[26] +#define col_salmon color_pallet[27] +#define col_sienna color_pallet[28] +#define col_silver color_pallet[29] +#define col_tan color_pallet[30] +#define col_turquoise color_pallet[31] +#define col_violet color_pallet[32] + + +#if USE_CORRECTED_COLORS == 0 + +const rgbpixel_t color_pallet[] = { + {0 , 0 , 0 }, // col_black + {255, 255, 255}, // col_white + {255, 0 , 0 }, // col_red + {0 , 255, 0 }, // col_green + {0 , 0 , 255}, // col_blue + {255, 165, 0 }, // col_orange + {255, 255, 0 }, // col_yellow + {0 , 255, 255}, // col_cyan + {255, 0 , 255}, // col_magenta + {128, 0 , 128}, // col_purple + {255, 192, 203}, // col_pink + {0 , 128, 128}, // col_teal + {0 , 255, 0 }, // col_lime + {75 , 0 , 130}, // col_indigo + {128, 0 , 0 }, // col_maroon + {0 , 0 , 128}, // col_navy + {128, 128, 0 }, // col_olive + {245, 245, 220}, // col_beige + {165, 42 , 42 }, // col_brown + {255, 127, 80 }, // col_coral + {255, 215, 0 }, // col_gold + {128, 128, 128}, // col_gray + {255, 255, 240}, // col_ivory + {240, 230, 140}, // col_khaki + {230, 230, 250}, // col_lavender + {255, 218, 185}, // col_peach + {204, 204, 255}, // col_periwinkle + {250, 128, 114}, // col_salmon + {160, 82 , 45}, // col_sienna + {192, 192, 192}, // col_silver + {210, 180, 140}, // col_tan + {64 , 224, 208}, // col_turquoise + {238, 130, 238} // col_violet +}; + +#else + +const rgbpixel_t color_pallet[] = +{ + {0 , 0 , 0 }, // col_black + {255, 255, 255}, // col_white + {255, 0 , 0 }, // col_red + {0 , 255, 0 }, // col_green + {0 , 0 , 255}, // col_blue + + {255, 128, 0 }, // col_orange + {255, 255, 0 }, // col_yellow + {0 , 255, 255}, // col_cyan + {255, 0 , 255}, // col_magenta + {170, 0 , 255}, // col_purple + {255, 170, 255}, // col_pink + {0 , 128, 128}, // col_teal + {128, 255, 0 }, // col_lime + {85 , 0 , 255}, // col_indigo + {128, 0 , 0 }, // col_maroon + {0 , 0 , 128}, // col_navy + {128, 128, 0 }, // col_olive + {255, 230, 204}, // col_beige + {153, 51 , 0 }, // col_brown + {255, 102, 102}, // col_coral + {204, 153, 0 }, // col_gold + {128, 128, 128}, // col_gray + {255, 255, 204}, // col_ivory + {204, 204, 0 }, // col_khaki + {204, 153, 255}, // col_lavender + {255, 204, 153}, // col_peach + {153, 153, 255}, // col_periwinkle + {255, 153, 102}, // col_salmon + {153, 76 , 0 }, // col_sienna + {204, 204, 204}, // col_silver + {204, 153, 102}, // col_tan + {0 , 204, 204}, // col_turquoise + {204, 0 , 255} // col_violet +}; +#endif + + + + +#endif diff --git a/z_old/old/anim-list.json b/z_old/old/anim-list.json new file mode 100644 index 0000000..fd3a27f --- /dev/null +++ b/z_old/old/anim-list.json @@ -0,0 +1,105 @@ +{ + "whitefills":[ + { + "name":"Bottom/Up Fill", + "speed":"", + "hue-range":"", + "param1":"Time: ", + "param2":"Const Light Delay: ", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + } + ], + "animations":[ + { + "name":"Rainbow", + "speed":"Speed: ", + "hue-range":"", + "param1":"", + "param2":"", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Hue Spectrum", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"", + "param2":"", + "check1":"Dir Rev ", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Color Pulse Cycle", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"", + "param2":"Colors: ", + "check1":"", + "check2":"", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Comets", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Random Decay", + "check3":"Shorter Tail ", + "check4":"50% Lum" + }, + { + "name":"Dashes", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Snakes", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"Mixed Colors ", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Sectors", + "speed":"Speed: ", + "hue-range":"Hue Range: ", + "param1":"Sectors: ", + "param2":"Colors: ", + "check1":"", + "check2":"Rotate ", + "check3":"", + "check4":"50% Lum" + }, + { + "name":"Fire", + "speed":"Speed: ", + "hue-range":"Run-time per: ", + "param1":"Cooling: ", + "param2":"Sparking: ", + "check1":"Cycle Colors ", + "check2":"", + "check3":"", + "check4":"50% Lum" + } + ] +} \ No newline at end of file diff --git a/z_old/old/anim-profile-common.json b/z_old/old/anim-profile-common.json new file mode 100644 index 0000000..87730d5 --- /dev/null +++ b/z_old/old/anim-profile-common.json @@ -0,0 +1,9 @@ +{ + "countdown": { + "min": 0, + "max": 98, + "hold": 1000, + "ramp": 500 + }, + "profile-index": 3 +} \ No newline at end of file diff --git a/z_old/old/anim-profile1.json b/z_old/old/anim-profile1.json new file mode 100644 index 0000000..96209ec --- /dev/null +++ b/z_old/old/anim-profile1.json @@ -0,0 +1,149 @@ +{ +"name": "Basic", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 1, + "hue": 40, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 2, + "hue": 80, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 150, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile2.json b/z_old/old/anim-profile2.json new file mode 100644 index 0000000..530f239 --- /dev/null +++ b/z_old/old/anim-profile2.json @@ -0,0 +1,149 @@ +{ +"name": "Rosey", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile3.json b/z_old/old/anim-profile3.json new file mode 100644 index 0000000..195e8c5 --- /dev/null +++ b/z_old/old/anim-profile3.json @@ -0,0 +1,149 @@ +{ +"name": "Bluish", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile4.json b/z_old/old/anim-profile4.json new file mode 100644 index 0000000..949c7ad --- /dev/null +++ b/z_old/old/anim-profile4.json @@ -0,0 +1,149 @@ +{ +"name": "USA", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile5.json b/z_old/old/anim-profile5.json new file mode 100644 index 0000000..38980f7 --- /dev/null +++ b/z_old/old/anim-profile5.json @@ -0,0 +1,149 @@ +{ +"name": "Summer", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile6.json b/z_old/old/anim-profile6.json new file mode 100644 index 0000000..5fa5a46 --- /dev/null +++ b/z_old/old/anim-profile6.json @@ -0,0 +1,149 @@ +{ +"name": "Winter", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile7.json b/z_old/old/anim-profile7.json new file mode 100644 index 0000000..f9205df --- /dev/null +++ b/z_old/old/anim-profile7.json @@ -0,0 +1,149 @@ +{ +"name": "Christmas", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profile8.json b/z_old/old/anim-profile8.json new file mode 100644 index 0000000..eb06cd8 --- /dev/null +++ b/z_old/old/anim-profile8.json @@ -0,0 +1,149 @@ +{ +"name": "Halloween", +"events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } +] +} \ No newline at end of file diff --git a/z_old/old/anim-profiles copy.json b/z_old/old/anim-profiles copy.json new file mode 100644 index 0000000..e50c0e4 --- /dev/null +++ b/z_old/old/anim-profiles copy.json @@ -0,0 +1,1203 @@ +{ + "countdown": { + "min": 0, + "max": 98, + "hold": 1000, + "ramp": 500 + }, + "profile-index": 3, + "profiles": [ + { + "name": "Profile 1", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 2", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 3", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 4", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 75, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 1, + "hue": 0, + "hue-range": 340, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 3, + "hue": 0, + "hue-range": 340, + "speed": 75, + "param1": 30, + "param2": 60, + "check1": false, + "check2": true, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 40, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 5", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Profile 5", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Wedding 7", + "events": [ + { + "anim": 0, + "hue": -1, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + }, + { + "name": "Wedding 8", + "events": [ + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 0, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + }, + { + "anim": 0, + "hue": 10, + "hue-range": 1, + "speed": 50, + "param1": 50, + "param2": 50, + "check1": false, + "check2": false, + "check3": false, + "check4": false + } + ] + } + ] +} \ No newline at end of file diff --git a/z_old/old/anim-settings.json b/z_old/old/anim-settings.json new file mode 100644 index 0000000..3889b0e --- /dev/null +++ b/z_old/old/anim-settings.json @@ -0,0 +1,97 @@ +{ + "twinkle":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Strobe":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Solid":{ + "col1": "#000000", + "col2": "#000000", + "density": 1, + "pwr": 255 + }, + "Fade":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "HueSwirl":{ + "col1": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Meteors":{ + "col1": "#000000", + "col2": "#000000", + "range": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Dashes":{ + "col1": "#000000", + "col2": "#000000", + "col3": "#000000", + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "DashesRange":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "segments": 1, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Fire":{ + "col1": "#000000", + "cool": 25, + "spark": 25, + "speed": 25, + "pwr": 255 + }, + "Rain":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "density": 1, + "speed": 25, + "pwr": 255 + }, + "Stacking":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Snake":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "pwr": 255 + }, + "Theater":{ + "col1": "#000000", + "col2": "#000000", + "range": 128, + "speed": 25, + "step": 20, + "pwr": 255 + } +} \ No newline at end of file diff --git a/z_old/old/app-events.json b/z_old/old/app-events.json new file mode 100644 index 0000000..fa7b393 --- /dev/null +++ b/z_old/old/app-events.json @@ -0,0 +1,90 @@ +{ + "index": 0, + "apps":[ + { + "name": "DSLR-Booth Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + { + "name": "FotoFliqs Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop", + "", + "", + "", + "" + ] + }, + { + "name": "Touchpix Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Preview", + "Notice!", + "Idle / Pause", + "Advertisement", + "Printing", + "Phone Input / QR Code / Apple Drop", + "", + "", + "", + "" + ] + }, + { + "name": "FotoZap Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + { + "name": "Twineit Configuration", + "events":[ + "White Fill / Countdown", + "Experience Selection", + "Select Sharing Screen", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + } + ] +} \ No newline at end of file diff --git a/z_old/old/ble.json b/z_old/old/ble.json new file mode 100644 index 0000000..8cd73ca --- /dev/null +++ b/z_old/old/ble.json @@ -0,0 +1,9 @@ +{ + "en": "true", + "core": 1, + "device-name": "ATA_COMM", + "key": "123456", + "service-uuid": "6E400001-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-rx": "6E400002-B5A3-F393-E0A9-E50E24DCCA9E", + "char-uuid-tx": "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" +} \ No newline at end of file diff --git a/z_old/old/board.json b/z_old/old/board.json new file mode 100644 index 0000000..2077f39 --- /dev/null +++ b/z_old/old/board.json @@ -0,0 +1,63 @@ +{ + "use_ver":"hwver15", + "hwver14":{ + "rgb1": 3, + "rgb2": 46, + "btn1": 8, + "btn2": 19, + "btn3": 0, + "buzzer": 37, + "touch1": 9, + "touch2": 10, + "touch3": 11, + "touch4": 12, + "touch5": 13, + "shield": 9, + "relay1": 1, + "relay2": 2, + "relay3": 3, + "relay4": 4, + "stat1": 17, + "stat2": 18, + "adc1": 20, + "oled_dc": 7, + "oled_rst": 6, + "oled_mosi": 5, + "oled_sck": 4, + "oled_cs": 15, + "ext1": 35, + "ext2": 36, + "rf433tx": 38, + "rf433rx": 16 + }, + "hwver15":{ + "rgb1": 3, + "rgb2": 9, + "btn1": 8, + "btn2": 18, + "btn3": 0, + "buzzer": 42, + "touch1": 11, + "touch2": 12, + "touch3": 13, + "touch4": 21, + "touch5": -1, + "shield": 14, + "relay1": 38, + "relay2": 48, + "relay3": 47, + "relay4": 21, + "stat1": 39, + "stat2": 37, + "adc1": 10, + "oled_dc": 7, + "oled_rst": 6, + "oled_mosi": 5, + "oled_sck": 4, + "oled_cs": 15, + "ext1": 41, + "ext2": -1, + "rf433tx": 40, + "rf433rx": 16 + } +} diff --git a/z_old/old/led-devices.json b/z_old/old/led-devices.json new file mode 100644 index 0000000..7f5fd74 --- /dev/null +++ b/z_old/old/led-devices.json @@ -0,0 +1,42 @@ +{ + "strip1": { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "pin": 3, + "i2s-ch": 0, + "core": 1 + }, + "strip2": { + "en": false, + "size": 20, + "chip": "SK6812", + "rgb-order": "grb", + "shift":0, + "offset": 0, + "power-div": 0, + "pin": 46, + "i2s-ch": 1, + "core": 0 + }, + "front-light": { + "en": true, + "relay": 0, + "core": 1, + "style": 0, + "delay": 0.75 + }, + "rear-light": { + "en": true, + "relay": 1, + "button": 2, + "min": 0.0, + "max": 100.0, + "ramp":3000, + "steps":8 + } +} \ No newline at end of file diff --git a/z_old/old/ramp-lights.json b/z_old/old/ramp-lights.json new file mode 100644 index 0000000..97a7052 --- /dev/null +++ b/z_old/old/ramp-lights.json @@ -0,0 +1,23 @@ +{ + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5 + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5 + } + ] +} \ No newline at end of file diff --git a/z_old/old/relays.json b/z_old/old/relays.json new file mode 100644 index 0000000..7e11224 --- /dev/null +++ b/z_old/old/relays.json @@ -0,0 +1,41 @@ +{ +"relays": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ] +} \ No newline at end of file diff --git a/z_old/old/system.json b/z_old/old/system.json new file mode 100644 index 0000000..8cc156a --- /dev/null +++ b/z_old/old/system.json @@ -0,0 +1,146 @@ +{ + "boardver": "board15", + "mode": 0, + "buttons": + [ + { + "en": true + }, + { + "en": true + }, + { + "en": true + } + ], +"pwmout": + [ + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": false, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + }, + { + "en": true, + "freq": 250, + "min": 0, + "max": 100, + "default": 0, + "vision": true, + "deltarate": 0 + } + ], + "ramp-lights": + [ + { + "en": true, + "relay-index": 0, + "button-index": 0, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5 + }, + { + "en": true, + "relay-index": 1, + "button-index": 1, + "min": 5.0, + "max": 100.0, + "step": 1.5, + "skip-count": 5 + } + + ], + "oled": { + "en": false, + "height": 64, + "width": 128 + }, + "t-sensor": { + "en": true, + "addr": 72, + "sp1": 85.0, + "fan-pwr1": 50.0, + "sp2": 90.0, + "fan-pwr2": 100.0, + "hyst": 1.0, + "relay": 3, + "interval": 5000 + }, + "adc": { + "ain1_factor": 1.0 + }, + "status-led":{ + "interval": 250 + }, + "strip1": { + "en": true, + "size": 138, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + "strip2": { + "en": true, + "size": 30, + "chip": "SK6812", + "rgb-order": "rgb", + "shift":-27, + "offset": 0, + "power-div": 0, + "i2s-ch": 0, + "core": 1 + }, + "rx433": { + "en": "true" + }, + "tx433": { + "en": "true" + }, + "ble":{ + "en": true, + "name": "Ata_Lights", + "core": 1 + }, + "wifi-client":{ + "en": true, + "mdns-name": "atadev", + "ssid": "padillaguest", + "pass": "padillaguest" + }, + "wifi-ap":{ + "ssid": "ATA_AP", + "append-id": true, + "pass": "12345678", + "ip": [192,168,10.1], + "gateway": [192,168,10,200], + "subnet": [255,255,255,0] + } +} diff --git a/z_old/old/touch-pins.json b/z_old/old/touch-pins.json new file mode 100644 index 0000000..d556439 --- /dev/null +++ b/z_old/old/touch-pins.json @@ -0,0 +1,30 @@ +{ + "touchpins": + [ + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + }, + { + "en": true, + "pin": 23, + "min": 100 + } + ] +} \ No newline at end of file diff --git a/z_old/wifi_temp.cpp b/z_old/wifi_temp.cpp new file mode 100644 index 0000000..4d2645f --- /dev/null +++ b/z_old/wifi_temp.cpp @@ -0,0 +1,1449 @@ + +/* + wifi is started in STATION mode and tries to connected to AP saved + in the creds.json file. It will keep trying to connect forever. If + it connects then the board LEDS will toggle, the buzzer will play a tune + and the IP address will be sent to the serial port. Also the device will + be discoverable as "http://atadev.local/" + + If credentials need to be changed then double reset can be done to + trigger the AP mode and a wifi config page will be available on address + 192.168.4.1. Credentials can be entered and applied then you can reset + the device so it connects with the correct credentials. + +*/ +#include "my_wifi.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "global.h" +#include +#include "common/my_buzzer.h" +#include "my_board.h" +#include "common/led_animation.h" +#include "led_strip.h" +#include "common/fileSystem.h" +#include "JsonConstrain.h" +#include "led_strip.h" +#include "my_oled.h" +#include "BTSerial.h" +#include "esp_log.h" + +#include "firmware_html.h" + +int RebootSystem = 0; +#define WIFI_TIMEOUT_MS 10000 + +static const char* tag = "wifi"; + +bool WifiClientConnected = false; +AsyncWebServer webServer(80); +DNSServer *dnsServer; +#define DNS_PORT 53 + +#define StartDelayedRebooth RebootSystem = 1000/BUTTON_UPDATE_PERIOD; // start reboot countdown for 1sec + +String ssid; +String passPhrase; + +String AP_SSID; +String AP_Pass; +String mDnsName; +String HostName; + +IPAddress local_IP(192,168,10,1); +IPAddress gateway(192,168,10,200); +IPAddress subnet(255,255,255,0); + +// for file manager page +String filesDropdownOptions((char*)0); +String dirDropdownOptions((char*)0); +String savePath((char*)0); // needed for storing file when editing a file +String savePathInput((char*)0); +const char* http_username = "admin"; +const char* http_password = "admin"; +const char* param_delete_path = "delete-path"; +const char* param_edit_path = "edit-path"; +const char* param_dir_pad = "dir-path"; +const char* param_edit_textarea = "edit-textarea"; +const char* param_save_path = "save-path"; +String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg"; +//const char* jquery = "/jquery-3.6.3.min.js"; + +void Init_Wifi_Task(void) +{ + File file = LittleFS.open("/cfg/wifi.json", "r"); + if(!file){ + ESP_LOGE(tag, "Failed to open wifi.json!"); + file.close(); + return; + } + + // Parse the JSON file + StaticJsonDocument<1024> doc; + DeserializationError error = deserializeJson(doc, file); + if(!error){ + JsonObject wifiJson = doc["wifi"]; + file.close(); + mDnsName = jsonConstrainString(tag, wifiJson,"mdns-name", "atadev"); + ESP_LOGV(tag, "mDnsName: %s", mDnsName.c_str()); + + if(jsonConstrainBool(tag, wifiJson, "en", false)){ + JsonObject j = doc["cred1"]; + Wifi_Read_Credentials( j ); + xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*10, NULL, 1, NULL, CONFIG_ARDUINO_RUNNING_CORE); + ESP_LOGV(tag, "Initialized WiFi (task created)..."); + } + + JsonObject ap = doc["ap"]; + AP_SSID = jsonConstrainString(tag, ap, "ssid", "ATA_AP"); + if(jsonConstrainBool(tag, ap, "append-id", true)){ + String macHex = String(chipInfo.macByte[1], HEX) + String(chipInfo.macByte[0], HEX); + AP_SSID += "_"; + macHex.toUpperCase(); + AP_SSID += macHex; + } + AP_Pass = jsonConstrainString(tag, ap, "pass", "12345678"); + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } +} + +void onWiFiEvent(WiFiEvent_t event) +{ + //Serial.printf("[WiFi-event] event: %d\n", event); + switch (event) { + case ARDUINO_EVENT_WIFI_READY: + //Serial.println("WiFi interface ready"); + break; + case ARDUINO_EVENT_WIFI_SCAN_DONE: + //Serial.println("Completed scan for access points"); + break; + case ARDUINO_EVENT_WIFI_STA_START: + //Serial.println("WiFi client started"); + break; + case ARDUINO_EVENT_WIFI_STA_STOP: + //Serial.println("WiFi clients stopped"); + break; + case ARDUINO_EVENT_WIFI_STA_CONNECTED: + WifiClientConnected = true; + //Serial.println("Connected to access point"); + break; + case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: + WifiClientConnected = false; + ESP_LOGV(tag,"WiFi Disconnected"); + //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: + //Serial.println("Authentication mode of access point has changed"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP: + ESP_LOGV(tag,"My IP: %s", WiFi.localIP().toString()); + Wifi_Start_MDNS(); + Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_STA_LOST_IP: + //Serial.println("Lost IP address and IP address is reset to 0"); + break; + case ARDUINO_EVENT_WPS_ER_SUCCESS: + //Serial.println("WiFi Protected Setup (WPS): succeeded in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_FAILED: + //Serial.println("WiFi Protected Setup (WPS): failed in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_TIMEOUT: + // Serial.println("WiFi Protected Setup (WPS): timeout in enrollee mode"); + break; + case ARDUINO_EVENT_WPS_ER_PIN: + //Serial.println("WiFi Protected Setup (WPS): pin code in enrollee mode"); + break; + case ARDUINO_EVENT_WIFI_AP_START: + //Serial.println("WiFi access point started"); + break; + case ARDUINO_EVENT_WIFI_AP_STOP: + //Serial.println("WiFi access point stopped"); + break; + case ARDUINO_EVENT_WIFI_AP_STACONNECTED: + //Serial.println("Client connected"); + break; + case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: + ESP_LOGV(tag,"SoftAP Client Disconnected"); + //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: + ESP_LOGV(tag,"SoftAP Client Connected"); + Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); + break; + case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: + //Serial.println("Received probe request"); + break; + case ARDUINO_EVENT_WIFI_AP_GOT_IP6: + //Serial.println("AP IPv6 is preferred"); + break; + case ARDUINO_EVENT_WIFI_STA_GOT_IP6: + //Serial.println("STA IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_GOT_IP6: + //Serial.println("Ethernet IPv6 is preferred"); + break; + case ARDUINO_EVENT_ETH_START: + //Serial.println("Ethernet started"); + break; + case ARDUINO_EVENT_ETH_STOP: + //Serial.println("Ethernet stopped"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + //Serial.println("Ethernet connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + //Serial.println("Ethernet disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + // Serial.println("Obtained IP address"); + break; + default: break; + } +} + +void Wifi_Task(void *parameters) +{ + // Extend watchdog timer + esp_task_wdt_init(2, false); + + Setup_WebServer_Handlers(&webServer); + WiFi.onEvent(onWiFiEvent); + WiFi.setHostname(mDnsName.c_str()); + WiFi.softAPConfig(local_IP, gateway, subnet); + WiFi.softAP(AP_SSID.c_str(), AP_Pass.c_str()); + Wifi_Start_MDNS(); + vTaskDelay(100); + webServer.begin(); + + // Choose WIFI Mode + if(commMode == COMM_WIFI_AP_BLE){ // AP Only + WiFi.mode(WIFI_AP); + while(1){ vTaskDelay(500 / portTICK_PERIOD_MS); } + }else{ // AP & Client (dslrbooth) + esp_wifi_set_ps(WIFI_PS_NONE); + WiFi.mode(WIFI_AP_STA); + Wifi_Client_Loop(); + } +} + +void Wifi_Client_Loop(){ + while(true){ + // Wifi status check loop + if(WiFi.status() == WL_CONNECTED){ + vTaskDelay(250); + continue; + } + + // Start WiFi Client + ESP_LOGD(tag, "Wifi trying stored creds: %s..%s",ssid.c_str(), passPhrase.c_str()); + WiFi.begin(ssid.c_str(), passPhrase.c_str()); + + //Wait until connected or timeout + ESP_LOGV(tag, "WiFi Connecting..."); + unsigned long startAttemptTime = millis(); + while(WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS){ + vTaskDelay(100 / portTICK_PERIOD_MS); + } + + // Delay if wifi connection fails and continue + if(WiFi.status() != WL_CONNECTED){ + ESP_LOGW(tag, "WIFI Connection Failed!"); + for(int i = 0; i < WIFI_TIMEOUT_MS/100; i++){ + vTaskDelay(100); + } + continue; + } + } +} + +void Wifi_Start_MDNS(void) +{ + ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str()); + if (!MDNS.begin(mDnsName.c_str())) { + ESP_LOGE(tag, "Error setting up MDNS responder!"); + }else{ + ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName); + } +} + +void Wifi_Read_Credentials(const JsonObject &credJson){ + if(credJson.isNull()){ // set generic defaults on error + ESP_LOGE(tag, "Failed to find wifi key"); + + ssid = "Generic"; + passPhrase = "password"; + //strncpy(ssid, "Generic", sizeof(ssid)-1); + //strncpy(passPhrase, "password", sizeof(passPhrase)-1); + return; + } + else{ + // TODO Restore String + ssid = jsonConstrainString(tag, credJson, "ssid", "default"); + passPhrase = jsonConstrainString(tag, credJson, "pass", "password"); + //strncpy(ssid, jsonConstrainString(tag, credJson, "ssid", "default").c_str(), sizeof(ssid)-1); + //strncpy(passPhrase, jsonConstrainString(tag, credJson, "pass", "password").c_str(), sizeof(passPhrase)-1); + } +} + +void Wifi_Save_Credentials(String newSSID, String newPass) +{ + // Open the credentials file writing + File credsFile = LittleFS.open("/cfg/wifi.json", "r+"); + if(!credsFile){ + ESP_LOGE(tag, "Failed to open wifi.json\n"); + return; + } + + // Parse the JSON file + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, credsFile); + if(!error){ + JsonObject cred1Json = doc.createNestedObject("cred1"); + + if(cred1Json){ + cred1Json["ssid"] = newSSID; + cred1Json["pass"] = newPass; + + // Clear file contents before saving the modified JSON + credsFile.seek(0); + + int written = serializeJson(doc, credsFile); // save to file + ESP_LOGD(tag, "Credentials saved... ssid: %s, pass: %s, size written: %d", newSSID, newPass, written); + } + credsFile.close(); + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } +} + +void Setup_WebServer_Handlers(AsyncWebServer* serv) +{ + serv->on("/dslrbooth", HTTP_GET, handleGET_DSLRBooth); + + serv->on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_AP && !WifiClientConnected){ + request->redirect("/wifi"); + }else{ + request->redirect("/home"); + } + }); + serv->on("/home", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); + }); + serv->on("/lights", HTTP_GET, [](AsyncWebServerRequest *request){ + sendHtmlFile("/www/lights.html", request, htmlProcessor); + }); + serv->on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/setup.html", request, htmlProcessor); + }); + serv->on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_MODE_APSTA){ + // TODO Disable navigation bar + } + sendHtmlFile("/www/wifi.html", request, htmlProcessor); + }); + serv->on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + sendHtmlFile("/www/files.html", request, htmlProcessor); + }); + serv->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handlePOST_Upload); + + serv->on("/download", HTTP_GET, [](AsyncWebServerRequest *request){ + if (request->hasParam("file")) { + String filename = request->getParam("file")->value(); + if (LittleFS.exists(filename)) { + fs::File file = LittleFS.open(filename, "r"); + if (file) { + request->send(LittleFS, filename, "application/octet-stream"); + file.close(); + return; + } + } + } + request->send(404, "text/plain", "File not found!"); + }); + serv->on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + + String filename = request->getParam(param_delete_path)->value(); + if(filename !="choose"){ + LittleFS.remove(filename.c_str()); + ESP_LOGD(tag, "Deleted file: %s", filename.c_str()); + } + + request->redirect("/files"); + }); + serv->on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + + String fileName = request->getParam(param_edit_path)->value(); + if(fileName =="new"){ + savePath = "/new.txt"; + } + else{ + savePath = fileName; + } + + ESP_LOGD(tag, "Edit file: %s", savePath.c_str()); + sendHtmlFile("/www/edit.html", request, htmlProcessor); + }); + serv->on("/save", HTTP_GET, [](AsyncWebServerRequest *request){ + if(!request->authenticate(http_username, http_password)){ + return request->requestAuthentication(); + } + String inputMessage((char*)0); + if (request->hasParam(param_edit_textarea)) { + inputMessage = request->getParam(param_edit_textarea)->value(); + } + if (request->hasParam(param_save_path)) { + savePath = request->getParam(param_save_path)->value(); + } + writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); + + request->redirect("/files"); + }); + serv->on("/update", HTTP_POST, handlePOST_Update, updateCallback); + + // Request Data + serv->on("/get", HTTP_GET, handleGET_Get); + // Post Data + serv->on("/post", HTTP_POST, handlePOST_Post, postFileUpload, postBody); + + serv->on("/generate_204", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/generate_204 ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/hotspot-detect.html", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/hotspot-detect.html ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/msftconnecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request){ + ESP_LOGD(tag, "/msftconnecttest.txt ... redirect to /wifi"); + request->redirect("/wifi"); + }); + + serv->on("/reg-stick", HTTP_POST, handlePOST_RegStick); + + serv->on("/firmware", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send_P(200, "text/html", firmware_html_page); + }); + + serv->onNotFound([](AsyncWebServerRequest *request){ + if(WiFi.getMode() == WIFI_MODE_APSTA){ + ESP_LOGD(tag, "OnNotFound Redirect to /wifi"); + request->redirect("/wifi"); + }else if(WiFi.getMode() == WIFI_MODE_AP){ + ESP_LOGD(tag, "OnNotFound Redirect to /home"); + request->redirect("/home"); + }else{ + request->send(404); + } + }); + + serv->on("/*", HTTP_GET, handleGET_SendFile); // send any file + +} + +void handlePOST_RegStick(AsyncWebServerRequest *request){ + +} + +bool getpostSuccess = false; +// +void handleGET_Get(AsyncWebServerRequest *request){ + if(request->hasParam("type", false, false)) { + const char* dataType = request->getParam("type", false, false)->value().c_str(); + + if(!strcmp(dataType, "app-events")){ // send json file + request->send(LittleFS, "/cfg/app-events.json", "application/json"); + return; + } + + if(!strcmp(dataType, "anim-profiles")){ // send json file + request->send(LittleFS, "/cfg/anim-profiles.json", "application/json"); + return; + } + + if(!strcmp(dataType, "anim-list")){ + request->send(LittleFS, "/cfg/anim-list.json", "application/json"); + return; + } + + if(!strcmp(dataType, "sys-summary")){ + JsonDocument doc; + CreateSysSummmaryPacket(doc); + String summary; + serializeJson(doc, summary); + request->send(200, "application/json", summary); + return; + } + + if(!strcmp(dataType, "wifi")){ + JsonDocument jsdoc; + jsdoc["ssid"] = ssid; + //jsdoc["pass"] = (int)strlen(passPhrase); + jsdoc["pass"] = passPhrase; + + String jsStr; + serializeJson(jsdoc, jsStr); + + request->send(200, "application/json", jsStr); + return; + } + } + + request->send(400, "text/plain", "Failed"); +} + +void CreateSysSummmaryPacket(JsonDocument &doc){ + JsonObject booth = doc.createNestedObject("booth"); + booth["mode"] = ""; + booth["app"] = sysProps.appName; + booth["strip1"] = (strip1) ? true : false, + booth["strip2"] = (strip2) ? true : false, + booth["front-light"] = true; + booth["rear-light"] = false; + booth["oled"] = (oled) ? true : false; + + //ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getHeapSize()-ESP.getFreeHeap()); + + JsonObject Temperature = doc.createNestedObject("temperature"); + Temperature["temp"] = sysProps.t_sensor.temperature; + Temperature["sp1"] = sysProps.t_sensor.Setpoint1; + doc["system"]["firm-ver"] = FIRMWARE_VER; + + JsonObject chip = doc.createNestedObject("chip"); + chip["flash-size"] = ""; + chip["flash-free"] = ""; + chip["ram-size"] = ESP.getHeapSize(); + chip["ram-free"] = ESP.getFreeHeap(); + + + + // Get the Wi-Fi RSSI + wifi_ap_record_t wifi_ap_info; + esp_err_t rssi_result = esp_wifi_sta_get_ap_info(&wifi_ap_info); + int rssi = 0; int wifi_ch = 0; + String encryp = ""; + if (rssi_result == ESP_OK) { + rssi = wifi_ap_info.rssi; + wifi_ch = wifi_ap_info.primary; + + wifi_auth_mode_t auth_mode = wifi_ap_info.authmode; + switch (auth_mode) { + case WIFI_AUTH_OPEN: + encryp = "Open"; + break; + case WIFI_AUTH_WEP: + encryp = "WEP"; + break; + case WIFI_AUTH_WPA_PSK: + encryp = "WPA_PSK"; + break; + case WIFI_AUTH_WPA2_PSK: + encryp = "WPA2_PSK"; + break; + case WIFI_AUTH_WPA_WPA2_PSK: + encryp = "WPA_WPA2_PSK"; + break; + case WIFI_AUTH_WPA2_ENTERPRISE: + encryp = "WPA2_ENT"; + break; + default: + encryp = "UNKNOWN"; + break; + } + } + + JsonObject wifi = doc.createNestedObject("wifi"); + wifi["client-ip"] = "XXX.XXX.XXX.XXX"; + wifi["mac-addr"] = chipInfo.macStr; + wifi["ch"] = wifi_ch; + wifi["rssi"] = rssi; + wifi["encryp"] = encryp; + + JsonObject ble = doc.createNestedObject("ble"); + ble["en"] = (sysProps.appIndex == DSLRBOOTH_INDEX)? false : true; + ble["clients"] = (BTDeviceConnected)? 1 : 0; + ble["ssid"] = BLEDeviceName; + +} + +void handlePOST_Post(AsyncWebServerRequest *request){ + ESP_LOGD(tag, "posts request.."); + if(!getpostSuccess) { request->send(400, "text/plain", "Error"); } + + request->send(200, "text/plain", "Ok"); +} + +void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { + getpostSuccess = false; + Serial.println("post file"); +} + + +// POST Requests +int packets, postTotal; +char postType[24]; +char *postData; +void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + if (!index){ + packets = 0; + int sectors = (total + 1023 + 64) / 1024; + postData = new char[1024 * sectors]; + } + + // Accumulate Data Chunks + memcpy(postData + index, data, len); + packets++; + + if((index + len) >= total){ + ESP_LOGD(tag, "index: %d, len: %d, total: %d", index, len, total); + ESP_LOGD(tag, "packets: %d", packets); + getpostSuccess = false; + + // Check if the request contains the necessary parameters + if (request->hasParam("type", false, false)) { + const char* dataType = request->getParam("type", false, false)->value().c_str(); + ESP_LOGD(tag, "data type: %s", dataType); + + if(!strcmp(dataType, "anim-profile-common") || !strcmp(dataType, "set-countdown")){ + JsonDocument profJson(1 * 1024); + DeserializationError error = deserializeJson(profJson, postData, total); + + if(!error){ + animProps.profileIndex = profJson["profile-index"].as(); + + JsonObject countdownJson = profJson["countdown"]; + animProps.frontLight.minDuty = countdownJson["min"].as(); + animProps.frontLight.maxDuty = countdownJson["max"].as(); + animProps.frontLight.holdTime = countdownJson["hold"].as(); + animProps.frontLight.rampTime = countdownJson["ramp"].as(); + + int indexChanged = animProps.profileIndex - countdownJson["profile-index"].as(); + animProps.profileIndex = countdownJson["profile-index"].as(); + if(indexChanged){ + load_animation_profileByIndex(animProps.profileIndex); + } + + // Save to disk/flash + if(dataType[0] == 'a'){ + if(updateJsonDocument(profJson, "/cfg/anim-profile-common.json")){ + Buzzer_Beep(150, 3000); + } + } + else{ + ESP_LOGD(tag, "Post:set-countdown, index: %d, min: %.1f, max: %.1f, hold: %d, ramp: %d", animProps.profileIndex, animProps.frontLight.minDuty, animProps.frontLight.maxDuty, animProps.frontLight.holdTime, animProps.frontLight.rampTime); + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } + + getpostSuccess = true; + //goto done; + } + else if(!strncmp(dataType, "anim-profile", 12)){ + if (strlen(dataType) > 12) { + int profile_index = (dataType[12] - '0' - 1) % 8; // extract index, assuming dataType has enough characters + DynamicJsonDocument profJson(4 * 1024); + DeserializationError error = deserializeJson(profJson, postData, total); + + if(!error){ + JsonArray eventsArray = profJson["events"]; + for(int i = 0; i < eventsArray.size(); i++ ){ + animProps.event[i].selfIndex = i; + animProps.event[i].animIndex = eventsArray[i]["anim"].as(); + animProps.event[i].hue = eventsArray[i]["hue"].as(); + animProps.event[i].hueRange = eventsArray[i]["hue-range"].as(); + animProps.event[i].speed = eventsArray[i]["speed"].as(); + animProps.event[i].param1 = eventsArray[i]["param1"].as(); + animProps.event[i].param2 = eventsArray[i]["param2"].as(); + animProps.event[i].check1 = eventsArray[i]["check1"].as(); + animProps.event[i].check2 = eventsArray[i]["check2"].as(); + animProps.event[i].check3 = eventsArray[i]["check3"].as(); + animProps.event[i].check4 = eventsArray[i]["check4"].as(); + animProps.event[i].countDown = 0; + } + + ESP_LOGD(tag, "Extracted/Updated active profile to animProps."); + + // Save to disk/flash + String filePath = "/cfg/anim-profile" + String(profile_index + 1) + ".json"; + if(updateJsonDocument(profJson, filePath.c_str())){ + Buzzer_Beep(150, 3000); + } + + }else{ + ESP_LOGE(tag, "Deserialization error for wifi.json"); + } + + getpostSuccess = true; + } + } + else if(!strcmp(dataType, "play-anim")){ + DynamicJsonDocument animData(1024); + DeserializationError error = deserializeJson(animData, postData, total); + if(!error){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = animData["index"].as(); + testEvent.animIndex = animData["anim"].as(); + testEvent.hue = animData["hue"].as(); + testEvent.hueRange = animData["hue-range"].as(); + testEvent.speed = animData["speed"].as(); + testEvent.param1 = animData["param1"].as(); + testEvent.param2 = animData["param2"].as(); + testEvent.check1 = animData["check1"].as(); + testEvent.check2 = animData["check2"].as(); + testEvent.check3 = animData["check3"].as(); + testEvent.check4 = animData["check4"].as(); + + if(testEvent.selfIndex != 0){ + testEvent.selfIndex = 1; + } + ESP_LOGI(tag, "Post:play-anim, index: %d, anim: %d, hue: %d, rng: %d, speed: %d, par1: %d, par2: %d, chk1: %d, chk2: %d, chk3: %d, chk4: %d", testEvent.selfIndex, testEvent.animIndex, testEvent.hue, testEvent.hueRange, testEvent.speed, testEvent.param1, testEvent.param2, testEvent.check1, testEvent.check2, testEvent.check3, testEvent.check4); + testEvent.countDown = 0; // set to zero so param1 determines countdown time + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + Buzzer_Beep(150, 3000); + }else{ + ESP_LOGE(tag, "Deserialization error for play-anim"); + } + + getpostSuccess = true; + } + else if(!strcmp(dataType, "wifi")){ + JsonDocument wifiCreds; + DeserializationError error = deserializeJson(wifiCreds, postData, total); + if(!error){ + //read credentials + //const char* new_ssid = wifiCreds["ssid"]; + //const char* new_pass = wifiCreds["pass"]; + + String new_ssid = wifiCreds["ssid"]; + String new_pass = wifiCreds["pass"]; + ESP_LOGV(tag, "SSID: %s PASS: %s", new_ssid, new_pass); + // Save Credentials if different + //if(strcmp(new_ssid, ssid)!=0 || strcmp(new_pass, passPhrase)!=0){ + if(new_ssid != ssid || new_pass != passPhrase) + Wifi_Save_Credentials(new_ssid, new_pass); + StartDelayedRebooth; + } + }else{ + ESP_LOGE(tag, "Deserialization error for wifi"); + } + + getpostSuccess = true; + } + else if(!strcmp(dataType, "set-pixel")){ + DynamicJsonDocument js(1024); + DeserializationError error = deserializeJson(js, postData, total); + if(!error){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = 1; + testEvent.animIndex = 81; + testEvent.hue = js["hue"].as(); + testEvent.param1 = js["index"].as(); + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + ESP_LOGD(tag, "Post: set-pixel, hue: %d, pixel: %d", testEvent.hue, testEvent.param1); + Buzzer_Beep(150, 3000); + }else{ + ESP_LOGE(tag, "Deserialization error for set-pixel"); + } + + Buzzer_Beep(50, 3000); + getpostSuccess = true; + } + else if(!strcmp(dataType, "clear-strip")){ + ANIMATION_EVENT testEvent; + testEvent.selfIndex = 1; + testEvent.animIndex = 80; + ESP_LOGD(tag, "Post: clear-strip"); + testEvent.type = EV_TEST; + PostNewEvent(testEvent); // eventIndex always = 1 + + getpostSuccess = true; + Buzzer_Beep(50, 3000); + } + else if(!strcmp(dataType, "setup-save")){ + DynamicJsonDocument js(1024); + DeserializationError error = deserializeJson(js, postData, total); + if(!error){ + // If app index is different open app-events.json and update + if(sysProps.appIndex != js["appindex"].as()){ + File file = LittleFS.open("/cfg/app-events.json", "r+"); + if(!file){ + ESP_LOGE(tag, "Failed to open app-events.json"); + file.close(); + return; + } + + DynamicJsonDocument doc(2048); + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if(!error){ + // Update index value + doc["index"] = js["appindex"].as(); + ESP_LOGD(tag, "New App Index = %d", doc["index"].as()); + + if(updateJsonDocument(doc, "/cfg/app-events.json")){ + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for app-events.json"); + } + } + }else{ + ESP_LOGE(tag, "Deserialization error for seteup-save"); + } + + // if any of the values are diiferent then open led-devices.json and update + //if app index is different open app-events.json and update + bool editJson = false; + //if(js["en1"].as() != strip1->size) { editJson = true; } + if(js["count1"].as() != strip1->size) { editJson = true; } + else if(js["shift1"].as() != strip1->shift) { editJson = true; } + else if(js["offset1"].as() != strip1->offset) { editJson = true; } + else if(js["rgb1"].as() != strip1->ledOrder) { editJson = true; } + else if(js["power1"].as() != strip1->powerDiv) { editJson = true; } + + if(editJson){ + File file = LittleFS.open("/cfg/led-devices.json", "r+"); + if(!file){ + ESP_LOGE(tag, "Failed to open led-devices.json"); + file.close(); + return; + } + + DynamicJsonDocument doc(2048); + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if(!error){ + JsonObject jsStrip1 = doc.createNestedObject("strip1"); // nested so it doesn't create a copy + + jsStrip1["en"] = true; + jsStrip1["size"] = js["count1"].as(); + jsStrip1["shift"] = js["shift1"].as(); + jsStrip1["offset"] = js["offset1"].as(); + jsStrip1["power-div"] = js["power1"].as(); + jsStrip1["rgb-order"] = js["rgb1"]; + + // Save Json File + if(updateJsonDocument(doc, "/cfg/led-devices.json")){ + Buzzer_Beep(150, 3000); + } + }else{ + ESP_LOGE(tag, "Deserialization error for led-devices.json"); + } + } + + ESP_LOGD(tag, "Post: setup-save"); + getpostSuccess = true; + Buzzer_Beep(150, 3000); + } + else if(!strcmp(dataType, "restart")){ + StartDelayedRebooth; + + ESP_LOGD(tag, "Post: restart"); + getpostSuccess = true; + Buzzer_Beep(150, 3000); + } + } + + // Delete the allocated buffer (owned by the request->_tempObject) + delete[] postData; + } +} + +bool isInternetConnected() +{ + if (WiFi.status() == WL_CONNECTED) { // check if WiFi is connected + WiFiClient client; + const int httpPort = 80; + if (client.connect("www.google.com", httpPort)) { // try to connect to Google + client.stop(); + return true; // Internet access available + } + } + return false; // Internet access not available +} + +void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + if (!index && request->hasParam("dir-path", true, false)) { + AsyncWebParameter* p = request->getParam("dir-path",true, false); + String path = p->value() + "/" + filename; + ESP_LOGD(tag, "upload path: %s", path.c_str()); + + request->_tempFile= LittleFS.open(path, "w"); + //fs::File file = LittleFS.open(path, "w"); + if (!request->_tempFile) { + ESP_LOGE(tag, "Failed to create file!"); + return; + } + }else{ + ESP_LOGE(tag, "dir-path Param not found.."); + } + + if(len && request->_tempFile){ + request->_tempFile.write(data, len); + } + + if(final){ + request->_tempFile.close(); + request->redirect("/files"); + ESP_LOGD(tag, "UploadEnd: %s, %u B", filename.c_str(), index+len); + } +} + +void handleGET_DSLRBooth(AsyncWebServerRequest *request) +{ + static int lastCountdown = 1000; + request->send(200, "text/plain"); // send this right away to avoid comm delays + + if(request->args() >= 1){ + int x = 0; + const char* event_type = request->arg( x ).c_str(); + + if(strcmp("countdown", event_type) == 0){ + int p = request->arg(1).toInt(); + if(p > 0 && p <= 100){ + animStatus.countStatus = p; + } + + // in case "coundown_start" was missed + if(animStatus.EventsIndex != ANIM_WHITEFILL_INDEX){ + animProps.event[0].countDown = lastCountdown; + animProps.event[0].type = EV_NORMAL; + PostNewEvent(animProps.event[0]); + } + } + else if(strcmp("countdown_start", event_type) == 0){ // index=0 + int count = request->arg(1).toInt(); // retrieve countdown value + //Log.traceln(" countdown = %d", count); + if(count < 1){count = 1;} + count *= 1000; + lastCountdown = count - 500; + animProps.event[0].countDown = lastCountdown; + animProps.event[0].type = EV_NORMAL; + PostNewEvent(animProps.event[0]); + } + else if(strcmp("sharing_screen", event_type) == 0){ + animProps.event[2].type = EV_NORMAL; + PostNewEvent(animProps.event[2]); + } + else if(strcmp("session_end", event_type) == 0){ //index=1 + animProps.event[1].type = EV_NORMAL; + PostNewEvent(animProps.event[1]); + } + else{ + // else if(strcmp_P("session_start", event_type) == 0){ + // TODO determine if ramp down is active depending on session type + + /* + const char* param1 = request->arg(1).c_str(); + if(strcmp("PrintOnly", param1)){ + animStatus.Animation = ANIM_START_PRINTONLY; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_print..\n\r"); + }else if(strcmp("PrintAndGIF", param1)){ + animStatus.Animation = ANIM_START_PRINTGIF; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_prnt_gif..\n\r"); + }else if(strcmp("OnlyGIF", param1)){ + animStatus.Animation = ANIM_START_GIFONLY; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_gif..\n\r"); + }else if(strcmp("Boomerang", param1)){ + animStatus.Animation = ANIM_START_BOOM; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_boom..\n\r"); + }else if(strcmp("Video", param1)){ + animStatus.Animation = ANIM_START_VIDEO; + xTaskNotifyGive( Strip1_Task_Handle ); + //Serial.println("ses_vid..\n\r"); + } + */ + } + } +} + +bool isFirmware = true; +bool updateSuccessful = false; +void handlePOST_Update(AsyncWebServerRequest *request) +{ + bool hasError = Update.hasError(); + String responseContent = hasError ? "failed" : "success"; + String responseType = hasError ? "text/plain" : "text/html"; + int responseCode = hasError ? 500 : 200; + + AsyncWebServerResponse *response = request->beginResponse(responseCode, responseType, responseContent); + response->addHeader("status", responseContent); + request->send(response); +} + +// handlePOST_Update Callback function to install the firmware or file system bin files +void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) +{ + // Do Once Code + if (!index) { + size_t fileSize = UPDATE_SIZE_UNKNOWN; + // Retrieve file-size parameter + if(request->hasParam("file-size", true, false)) { // make sure param is from POST request or it will not find it + fileSize = request->getParam("file-size", true, false)->value().toInt(); + } + + // register progress callback function + Update.onProgress(updateFirmwareProgress); + updateSuccessful = false; + + //file name checks + if (filename.startsWith("ata_fw")) { + isFirmware = true; + if (!Update.begin(fileSize, U_FLASH, BoardLED2)) { + ESP_LOGD(tag, "Firmware update failed to start"); + return; + } + ESP_LOGD(tag, "Updating Firmware with: %s Size:%d ", filename.c_str(), fileSize); + } + else if (filename.startsWith("ata_fs")) { + isFirmware = false; + if (!Update.begin(fileSize, U_SPIFFS, BoardLED2)) { + ESP_LOGD(tag, "LittleFS update failed to start"); + return; + } + ESP_LOGD(tag, "Updating File System with: %s Size:%d ", filename.c_str(), fileSize); + } + else { + ESP_LOGD(tag, "Unsupported file type"); + return; + } + } + + // Writing... + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + } + } + + // All Done... + if (final) { + vTaskDelay(100); + if(isFirmware) { // firmware update + if (Update.end(true)) { + RebootSystem = true; + updateSuccessful = true; + ESP_LOGD(tag, "firmware update successful: %d", index + len); + } + else { + Update.printError(Serial); + } + } + else{ // file system update + if(Update.end(true)) { + updateSuccessful = true; + ESP_LOGD(tag, "file system update successful!"); + } + else { + Update.printError(Serial); + } + } + } +} + +// Server requested files that aren't template processed +void handleGET_SendFile(AsyncWebServerRequest *request)// try this later "^/(img|favicon).*" +{ + String filePath = request->url(); + const char* ext = getFileExtension(filePath.c_str()); + const char* contentType = getFileType(ext); + + if( contentType == NULL ){ + request->send(404); + return; + } + + if(filePath.c_str()[0] != '/'){ + filePath = '/' + filePath; + } + + if (!strcmp(ext,"html") || !strcmp(ext, "htm") || !strcmp(ext, "css") || !strcmp(ext, "js")) { + if(!filePath.startsWith("/www/")){ + filePath = "/www" + filePath; + } + } + + ESP_LOGD(tag, "Sent: %s", filePath.c_str()); + request->send(LittleFS, filePath, contentType); +} + +String listDir(String directoryList[], int count) +{ + String listedFiles; + + for (int i = 0; i < count; i++) { + // directory html + listedFiles += "Dir: "; + listedFiles += directoryList[i]; + listedFiles += "/-\n"; + + filesDropdownOptions += "\n"; + + dirDropdownOptions += "\n"; + + File dir = LittleFS.open(directoryList[i]); + File file = dir.openNextFile(); + while (file) { + String fileName = file.name(); + if (!file.isDirectory()) { + //Serial.println(" File: " + String(file.name())); + listedFiles += "  "; + listedFiles += fileName; + listedFiles += ""; + listedFiles += convertFileSize(file.size()); + listedFiles += "\n"; + + filesDropdownOptions += "\n"; + } + file = dir.openNextFile(); + } + dir.close(); + } + + return listedFiles; +} + +char* readFile(fs::FS &fs, const char* path) { + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) { + return nullptr; + } + + size_t fileSize = file.size(); + char* fileContent = new char[fileSize + 1]; // +1 for null-terminator + + size_t bytesRead = file.readBytes(fileContent, fileSize); + file.close(); + + fileContent[bytesRead] = '\0'; // Set null-terminating character + + if (bytesRead != fileSize) { + delete[] fileContent; + return nullptr; + } + + return fileContent; +} + +void writeFile(fs::FS &fs, const char * path, const char * message) +{ + File file = fs.open(path, "w"); + if(!file){ + return; + } + file.print(message); + file.close(); +} + +// Send html file with template processing {{VAR}} +void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)) +{ + ESP_LOGD(tag, "Sent file: %s", filePath); + File file = LittleFS.open(filePath, "r"); + const char* htmlFile = readFile(LittleFS, filePath); + + //String processedData = varReplace(htmlFile, htmlProcessor); + String processedData = varReplace(htmlFile, callback); + request->send(200, "text/html", processedData); +} + +const char* convertFileSize(const size_t bytes) { + static char fileSizeBuffer[16]; // PreAllocated buffer for the file size + + if (bytes < 1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes); + } else if (bytes < 1024*1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", static_cast(bytes) / 1024.0); + } else if (bytes < 1024*1024*1024) { + snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", static_cast(bytes) / 1048576.0); + } else { + fileSizeBuffer[0] = '\0'; // Empty string if the file size is too large + } + + return fileSizeBuffer; +} + +// Callback template processor +String htmlProcessor(const String& var) +{ + if(var == "NAVBAR"){ return String("\n"); } + + if(var == "ALLOWED_EXTENSIONS_EDIT"){ return allowedExtensionsForEdit; } + + if(var == "FS_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); } + + if(var == "FS_USED_BYTES"){ return convertFileSize(LittleFS.usedBytes()); } + + if(var == "FS_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } + + if(var == "LISTED_FILES"){ + filesDropdownOptions = ""; // clear out + dirDropdownOptions = ""; // clear out + String directories[8]; + int dirCount = 0; + getAllDirectories(directories, dirCount); + + return listDir(directories, dirCount); + } + + if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; } + + if(var == "DIR_LIST"){ return dirDropdownOptions; } + + if(var == "SAVE_PATH_INPUT"){ + if(savePath == "/new.txt"){ + return String(""; + } + else{ + return String(""; + } + } + + if(var == "FIRM_VER"){ return FIRMWARE_VER; } + + return var; +} + +String HomeHtmlProcessor(const String& var) { + if(var == "NAVBAR"){ return String("\n"); } + + if (var == "APP_NAME") { + return sysProps.appName; + } + if (var == "OLED") { + return "No"; + } + if (var == "STRIP1") { + return (strip1) ? "Yes" : "No"; + } + if (var == "STRIP2") { + return (strip2) ? "Yes" : "No"; + } + if (var == "FRONT_LIGHT") { + return (animProps.frontLight.enabled) ? "Yes" : "No"; + } + if (var == "REAR_LIGHT") { + return (animProps.rearLight.enabled) ? "Yes" : "No"; + } + if (var == "FIRMWARE") { + return FIRMWARE_VER; + } + if (var == "BOOTH_T") { + return String(sysProps.t_sensor.temperature) + "F"; + } + if (var == "SETPOINT") { + return String(sysProps.t_sensor.Setpoint1) + "F"; + } + if (var == "FLASH_SIZE") { + return convertFileSize(ESP.getSketchSize()); + } + if (var == "FLASH_FREE") { + return convertFileSize(ESP.getFreeSketchSpace()); + } + if (var == "HEAP_SIZE") { + return convertFileSize(ESP.getHeapSize()); + } + if (var == "HEAP_FREE") { + return convertFileSize(ESP.getFreeHeap()); + } + if (var == "CPU_FREQ") { + return String(ESP.getCpuFreqMHz()) + "Mhz"; + } + if (var == "IP") { + return WiFi.localIP().toString(); + } + if (var == "MAC") { + return chipInfo.macStr; + } + if (var == "SSID") { + return WiFi.SSID(); + } + if (var == "RSSI") { + return String(WiFi.RSSI()); + } + if (var == "WIFI_CH") { + return String(WiFi.channel()); + } + if (var == "ENCRYP") { + return String(WiFi.encryptionType(0)); + } + if (var == "AP_SSID") { + return WiFi.softAPSSID(); + } + if (var == "AP_CLIENTS") { + return String(WiFi.softAPgetStationNum()); + } + if (var == "BLE") { + return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No"; + } + if (var == "BLE_SSID") { + return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : ""; + } + if (var == "BLE_CLIENTS") { + return (BTDeviceConnected) ? "1" : "0"; + } + if (var == "AP_MAC") { + return getSoftAPMacAddress(); + } + + + // Return an empty string if the variable is not recognized + return var; +} + +String getSoftAPMacAddress() { + uint8_t mac[6]; + WiFi.softAPmacAddress(mac); + + String macString = ""; + for (int i = 0; i < 6; i++) { + macString += String(mac[i], HEX); + if (i < 5) macString += ":"; + } + return macString; +} + +// Finds segments between {{VAR}} and calls a callback function to replace VAR with new content +String varReplace(const String& input, String (*callback)(const String&)) { + if (input.isEmpty()) { + return input; + } + + String result; + int startPos = 0; + int start = input.indexOf("{{", startPos); + + while (start != -1) { + result += input.substring(startPos, start); + + int end = input.indexOf("}}", start + 2); + if (end == -1) { + break; + } + + String segment = input.substring(start + 2, end); + int segmentLength = segment.length(); + if (segmentLength <= 32) { + String replacement = callback(segment); + result += replacement; + } else { + result += input.substring(start, end + 2); // Include the original segment if it exceeds the limit + } + + startPos = end + 2; + start = input.indexOf("{{", startPos); + } + + result += input.substring(startPos); + return result; +} + +const char* getFileExtension(const char* filename) { + size_t dotPos = strlen(filename); + while (dotPos > 0 && filename[dotPos - 1] != '.') { + dotPos--; + } + + if (dotPos != 0 && dotPos < strlen(filename) - 1) { + return &filename[dotPos]; + } + + return ""; // Empty string if no extension found +} + +const char* getFileType(const char* ext) { + if (strcmp(ext, "png") == 0) { + return "image/png"; + } else if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) { + return "image/jpeg"; + } else if (strcmp(ext, "gif") == 0) { + return "image/gif"; + } else if (strcmp(ext, "ico") == 0) { + return "image/x-icon"; + } else if (strcmp(ext, "txt") == 0) { + return "text/plain"; + } else if (strcmp(ext, "css") == 0) { + return "text/css"; + } else if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) { + return "text/html"; + } else if (strcmp(ext, "js") == 0) { + return "text/javascript"; + } else if (strcmp(ext, "json") == 0) { + return "application/json"; + } else { + return ""; + } +} + +// Serial print firmware or file system update progress +void updateFirmwareProgress(size_t progress, size_t total) +{ + static int lastProg = -1; + int firmwareProgress = (progress * 100)/ total; + + if(firmwareProgress != lastProg){ + Serial.printf("Progress: %u%%\r", firmwareProgress); + lastProg = firmwareProgress; + //TODO Add buzzer tune while uploading firmware + //Buzzer_Play_Tune(TUNE_DOWNLOADING,1,true,false); + } +} + +