Merge branch '0_15' into fw1906_0_15

This commit is contained in:
Blaž Kristan 2024-03-20 19:51:32 +01:00 committed by GitHub
commit 78096803ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1926 additions and 640 deletions

View File

@ -1,5 +1,19 @@
## WLED changelog ## WLED changelog
#### Build 2403190
- limit max PWM frequency (fix incorrect PWM resolution)
- Segment UI bugfix
- Updated AsyncWebServer (by @wlillmmiles)
- Simpler boot preset (fix for #3806)
- Effect: Fix for 2D Drift animation (#3816 by @BaptisteHudyma)
- Effect: Add twin option to 2D Drift
- MQTT cleanup
- DDP: Support sources that don't push (#3833 by @willmmiles)
- Usermod: Tetris AI usermod (#3711 by @muebau)
#### Build 2403171
- merge 0.14.2 changes into 0.15
#### Build 2403070 #### Build 2403070
- Add additional segment options when controlling over e1.31 (#3616 by @demophoon) - Add additional segment options when controlling over e1.31 (#3616 by @demophoon)
- LockedJsonResponse: Release early if possible (#3760 by @willmmiles) - LockedJsonResponse: Release early if possible (#3760 by @willmmiles)
@ -120,6 +134,26 @@
- send UDP/WS on segment change - send UDP/WS on segment change
- pop_back() when removing last segment - pop_back() when removing last segment
#### Build 2403170
- WLED 0.14.2 release
#### Build 2403110
- Beta WLED 0.14.2-b2
- New AsyncWebServer (improved performance and reduced memory use)
- New builds for ESP8266 with 160MHz CPU clock
- Fixing stairway usermod and adding buildflags (#3758 by @lost-hope)
- Fixing a potential array bounds violation in ESPDMX
- Reduced RAM usage (moved strings and TZ data (by @willmmiles) to PROGMEM)
- LockedJsonResponse: Release early if possible (by @willmmiles)
#### Build 2402120
- Beta WLED 0.14.2-b1
- Possible fix for #3589 & partial fix for #3605
- Prevent JSON buffer clear after failed lock attempt
- Multiple analog button fix for #3549
- UM Audioreactive: add two compiler options (#3732 by @wled-install)
- Fix for #3693
#### Build 2401141 #### Build 2401141
- Official release of WLED 0.14.1 - Official release of WLED 0.14.1
- Fix for #3566, #3665, #3672 - Fix for #3566, #3665, #3672

View File

@ -2,6 +2,16 @@
Here are a few suggestions to make it easier for you to contribute! Here are a few suggestions to make it easier for you to contribute!
### Describe your PR
Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing.
A good description helps us to review and understand your proposed changes. For example, you could say a few words about
* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.)
* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code)
* testing you performed, known limitations, open ends you possibly could not solve.
* any areas where you like to get help from an experienced maintainer (yes WLED has become big 😉)
### Target branch for pull requests ### Target branch for pull requests
Please make all PRs against the `0_15` branch. Please make all PRs against the `0_15` branch.

569
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -144,7 +144,7 @@ lib_deps =
fastled/FastLED @ 3.6.0 fastled/FastLED @ 3.6.0
IRremoteESP8266 @ 2.8.2 IRremoteESP8266 @ 2.8.2
makuna/NeoPixelBus @ 2.7.8 makuna/NeoPixelBus @ 2.7.8
https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.1.0 https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.2.0
# for I2C interface # for I2C interface
;Wire ;Wire
# ESP-NOW library # ESP-NOW library
@ -315,15 +315,9 @@ lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder monitor_filters = esp8266_exception_decoder
[env:nodemcuv2_160] [env:nodemcuv2_160]
board = nodemcuv2 extends = env:nodemcuv2
platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_4m1m}
board_build.f_cpu = 160000000L board_build.f_cpu = 160000000L
build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D
build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED_DISABLE_2D
lib_deps = ${esp8266.lib_deps}
monitor_filters = esp8266_exception_decoder
[env:esp8266_2m] [env:esp8266_2m]
board = esp_wroom_02 board = esp_wroom_02
@ -335,14 +329,9 @@ build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02
lib_deps = ${esp8266.lib_deps} lib_deps = ${esp8266.lib_deps}
[env:esp8266_2m_160] [env:esp8266_2m_160]
board = esp_wroom_02 extends = env:esp8266_2m
platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_2m512k}
board_build.f_cpu = 160000000L board_build.f_cpu = 160000000L
build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02_160
build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02
lib_deps = ${esp8266.lib_deps}
[env:esp01_1m_full] [env:esp01_1m_full]
board = esp01_1m board = esp01_1m
@ -355,15 +344,10 @@ build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_D
lib_deps = ${esp8266.lib_deps} lib_deps = ${esp8266.lib_deps}
[env:esp01_1m_full_160] [env:esp01_1m_full_160]
board = esp01_1m extends = env:esp01_1m_full
platform = ${common.platform_wled_default}
platform_packages = ${common.platform_packages}
board_build.ldscript = ${common.ldscript_1m128k}
board_build.f_cpu = 160000000L board_build.f_cpu = 160000000L
build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA
build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA
; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM
lib_deps = ${esp8266.lib_deps}
[env:esp32dev] [env:esp32dev]
board = esp32dev board = esp32dev

View File

@ -0,0 +1,117 @@
/******************************************************************************
* @file : gridbw.h
* @brief : contains the tetris grid as binary so black and white version
******************************************************************************
* @attention
*
* Copyright (c) muebau 2023
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __GRIDBW_H__
#define __GRIDBW_H__
#include <iterator>
#include <vector>
#include "pieces.h"
using namespace std;
class GridBW
{
private:
public:
uint8_t width;
uint8_t height;
std::vector<uint32_t> pixels;
GridBW(uint8_t width, uint8_t height):
width(width),
height(height),
pixels(height)
{
if (width > 32)
{
throw std::invalid_argument("maximal width is 32");
}
}
void placePiece(Piece* piece, uint8_t x, uint8_t y)
{
for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)
{
pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width);
}
}
void erasePiece(Piece* piece, uint8_t x, uint8_t y)
{
for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)
{
pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width);
}
}
bool noCollision(Piece* piece, uint8_t x, uint8_t y)
{
//if it touches a wall it is a collision
if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height)
{
return false;
}
for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++)
{
if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))])
{
return false;
}
}
return true;
}
void findLandingPosition(Piece* piece)
{
// move down until the piece bumps into some occupied pixels or the 'wall'
while (noCollision(piece, piece->x, piece->landingY))
{
piece->landingY++;
}
//at this point the positon is 'in the wall' or 'over some occupied pixel'
//so the previous position was the last correct one (clamped to 0 as minimum).
piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0;
}
void cleanupFullLines()
{
uint8_t offset = 0;
//from "height - 1" to "0", so from bottom row to top
for (uint8_t row = height; row-- > 0; )
{
//full line?
if (isLineFull(row))
{
offset++;
pixels[row] = 0x0;
continue;
}
if (offset > 0)
{
pixels[row + offset] = pixels[row];
pixels[row] = 0x0;
}
}
}
bool isLineFull(uint8_t y)
{
return pixels[y] == (uint32_t)((1 << width) - 1);
}
};
#endif /* __GRIDBW_H__ */

View File

@ -0,0 +1,132 @@
/******************************************************************************
* @file : gridcolor.h
* @brief : contains the tetris grid as 8bit indexed color version
******************************************************************************
* @attention
*
* Copyright (c) muebau 2023
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __GRIDCOLOR_H__
#define __GRIDCOLOR_H__
#include <stdint.h>
#include <stdbool.h>
#include <vector>
#include "gridbw.h"
#include "gridcolor.h"
using namespace std;
class GridColor
{
private:
public:
uint8_t width;
uint8_t height;
GridBW gridBW;
std::vector<uint8_t> pixels;
GridColor(uint8_t width, uint8_t height):
width(width),
height(height),
gridBW(width, height),
pixels(width* height)
{}
void clear()
{
for (uint8_t y = 0; y < height; y++)
{
gridBW.pixels[y] = 0x0;
for (int8_t x = 0; x < width; x++)
{
*getPixel(x, y) = 0;
}
}
}
void placePiece(Piece* piece, uint8_t x, uint8_t y)
{
for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++)
{
for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++)
{
if (piece->getPixel(pieceX, pieceY))
{
*getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex;
}
}
}
}
void erasePiece(Piece* piece, uint8_t x, uint8_t y)
{
for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++)
{
for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++)
{
if (piece->getPixel(pieceX, pieceY))
{
*getPixel(x + pieceX, y + pieceY) = 0;
}
}
}
}
void cleanupFullLines()
{
uint8_t offset = 0;
//from "height - 1" to "0", so from bottom row to top
for (uint8_t y = height; y-- > 0; )
{
if (gridBW.isLineFull(y))
{
offset++;
for (uint8_t x = 0; x < width; x++)
{
pixels[y * width + x] = 0;
}
continue;
}
if (offset > 0)
{
if (gridBW.pixels[y])
{
for (uint8_t x = 0; x < width; x++)
{
pixels[(y + offset) * width + x] = pixels[y * width + x];
pixels[y * width + x] = 0;
}
}
}
}
gridBW.cleanupFullLines();
}
uint8_t* getPixel(uint8_t x, uint8_t y)
{
return &pixels[y * width + x];
}
void sync()
{
for (uint8_t y = 0; y < height; y++)
{
gridBW.pixels[y] = 0x0;
for (int8_t x = 0; x < width; x++)
{
gridBW.pixels[y] <<= 1;
if (*getPixel(x, y) != 0)
{
gridBW.pixels[y] |= 0x1;
}
}
}
}
};
#endif /* __GRIDCOLOR_H__ */

View File

@ -0,0 +1,184 @@
/******************************************************************************
* @file : pieces.h
* @brief : contains the tetris pieces with their colors indecies
******************************************************************************
* @attention
*
* Copyright (c) muebau 2022
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __PIECES_H__
#define __PIECES_H__
#include <stdint.h>
#include <stdbool.h>
#include <bitset>
#include <cstddef>
#include <cassert>
#include <iostream>
#define numPieces 7
struct PieceRotation
{
uint8_t width;
uint8_t height;
uint16_t rows;
};
struct PieceData
{
uint8_t rotCount;
uint8_t colorIndex;
PieceRotation rotations[4];
};
PieceData piecesData[numPieces] = {
// I
{
2,
1,
{
{ 1, 4, 0b0001000100010001},
{ 4, 1, 0b0000000000001111}
}
},
// O
{
1,
2,
{
{ 2, 2, 0b0000000000110011}
}
},
// Z
{
2,
3,
{
{ 3, 2, 0b0000000001100011},
{ 2, 3, 0b0000000100110010}
}
},
// S
{
2,
4,
{
{ 3, 2, 0b0000000000110110},
{ 2, 3, 0b0000001000110001}
}
},
// L
{
4,
5,
{
{ 2, 3, 0b0000001000100011},
{ 3, 2, 0b0000000001110100},
{ 2, 3, 0b0000001100010001},
{ 3, 2, 0b0000000000010111}
}
},
// J
{
4,
6,
{
{ 2, 3, 0b0000000100010011},
{ 3, 2, 0b0000000001000111},
{ 2, 3, 0b0000001100100010},
{ 3, 2, 0b0000000001110001}
}
},
// T
{
4,
7,
{
{ 3, 2, 0b0000000001110010},
{ 2, 3, 0b0000000100110001},
{ 3, 2, 0b0000000000100111},
{ 2, 3, 0b0000001000110010}
}
},
};
class Piece
{
private:
public:
uint8_t x;
uint8_t y;
PieceData* pieceData;
uint8_t rotation;
uint8_t landingY;
Piece(uint8_t pieceIndex = 0):
x(0),
y(0),
rotation(0),
landingY(0)
{
this->pieceData = &piecesData[pieceIndex];
}
void reset()
{
this->rotation = 0;
this->x = 0;
this->y = 0;
this->landingY = 0;
}
uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width)
{
if (x < width)
{
//shift the row with the "top-left" position to the "x" position
auto shiftx = (width - 1) - x;
auto topleftx = (getRotation().width - 1);
auto finalShift = shiftx - topleftx;
auto row = getRow(y);
auto finalResult = row << finalShift;
return finalResult;
}
return 0xffffffff;
}
uint8_t getRow(uint8_t y)
{
if (y < 4)
{
return (getRotation().rows >> (12 - (4 * y))) & 0xf;
}
return 0xf;
}
bool getPixel(uint8_t x, uint8_t y)
{
if(x > getRotation().width - 1 || y > getRotation().height - 1 )
{
return false;
}
if (x < 4 && y < 4)
{
return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1;
}
return false;
}
PieceRotation getRotation()
{
return this->pieceData->rotations[rotation];
}
};
#endif /* __PIECES_H__ */

View File

@ -0,0 +1,64 @@
/******************************************************************************
* @file : rating.h
* @brief : contains the tetris rating of a grid
******************************************************************************
* @attention
*
* Copyright (c) muebau 2022
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __RATING_H__
#define __RATING_H__
#include <stdint.h>
#include <float.h>
#include <stdbool.h>
#include <math.h>
#include <vector>
#include "rating.h"
using namespace std;
class Rating
{
private:
public:
uint8_t minHeight;
uint8_t maxHeight;
uint16_t holes;
uint8_t fullLines;
uint16_t bumpiness;
uint16_t aggregatedHeight;
double score;
uint8_t width;
std::vector<uint8_t> lineHights;
Rating(uint8_t width):
width(width),
lineHights(width)
{
reset();
}
void reset()
{
this->minHeight = 0;
this->maxHeight = 0;
for (uint8_t line = 0; line < this->width; line++)
{
this->lineHights[line] = 0;
}
this->holes = 0;
this->fullLines = 0;
this->bumpiness = 0;
this->aggregatedHeight = 0;
this->score = -DBL_MAX;
}
};
#endif /* __RATING_H__ */

View File

@ -0,0 +1,33 @@
# Tetris AI effect usermod
This usermod brings you a effect brings a self playing Tetris game. The mod needs version 0.14 or above as it is based on matrix support. The effect was tested on an ESP32 with a WS2812B 16x16 matrix.
Version 1.0
## Installation
Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'.
## Usage
It is best to set the background color to black, the border color to light grey and the game over color (foreground) to dark grey.
### Sliders and boxes
#### Sliders
* speed: speed the game plays
* look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2)
* intelligence: how good the AI will play
* Rotate color: make the colors shift (rotate) every few cicles
* Mistakes free: how many good moves between mistakes (if activated)
#### Checkboxes
* show next: if true a space of 5 pixels from the right is used to show the next pieces. The whole segment is used for the grid otherwise.
* show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces
* mistakes: if true the worst instead of the best move is choosen every few moves (read above)
## Best results
If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party.

View File

@ -0,0 +1,302 @@
/******************************************************************************
* @file : ai.h
* @brief : contains the heuristic
******************************************************************************
* @attention
*
* Copyright (c) muebau 2023
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __AI_H__
#define __AI_H__
#include "gridbw.h"
#include "rating.h"
using namespace std;
class TetrisAI
{
private:
public:
double aHeight;
double fullLines;
double holes;
double bumpiness;
bool findWorstMove = false;
uint8_t countOnes(uint32_t vector)
{
uint8_t count = 0;
while (vector)
{
vector &= (vector - 1);
count++;
}
return count;
}
void updateRating(GridBW grid, Rating* rating)
{
rating->minHeight = 0;
rating->maxHeight = 0;
rating->holes = 0;
rating->fullLines = 0;
rating->bumpiness = 0;
rating->aggregatedHeight = 0;
fill(rating->lineHights.begin(), rating->lineHights.end(), 0);
uint32_t columnvector = 0x0;
uint32_t lastcolumnvector = 0x0;
for (uint8_t row = 0; row < grid.height; row++)
{
columnvector |= grid.pixels[row];
//first (highest) column makes it
if (rating->maxHeight == 0 && columnvector)
{
rating->maxHeight = grid.height - row;
}
//if column vector is full we found the minimal height (or it stays zero)
if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1)))
{
rating->minHeight = grid.height - row;
}
//line full if all ones in mask :-)
if (grid.isLineFull(row))
{
rating->fullLines++;
}
//holes are basically a XOR with the "full" columns
rating->holes += countOnes(columnvector ^ grid.pixels[row]);
//calculate the difference (XOR) between the current column vector and the last one
uint32_t columnDelta = columnvector ^ lastcolumnvector;
//process every new column
uint8_t index = 0;
while (columnDelta)
{
//if this is a new column
if (columnDelta & 0x1)
{
//update hight of this column
rating->lineHights[(grid.width - 1) - index] = grid.height - row;
// update aggregatedHeight
rating->aggregatedHeight += grid.height - row;
}
index++;
columnDelta >>= 1;
}
lastcolumnvector = columnvector;
}
//compare every two columns to get the difference and add them up
for (uint8_t column = 1; column < grid.width; column++)
{
rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]);
}
rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness));
}
TetrisAI(): TetrisAI(-0.510066, 0.760666, -0.35663, -0.184483)
{}
TetrisAI(double aHeight, double fullLines, double holes, double bumpiness):
aHeight(aHeight),
fullLines(fullLines),
holes(holes),
bumpiness(bumpiness)
{}
void findBestMove(GridBW grid, Piece *piece)
{
vector<Piece> pieces = {*piece};
findBestMove(grid, &pieces);
*piece = pieces[0];
}
void findBestMove(GridBW grid, std::vector<Piece> *pieces)
{
findBestMove(grid, pieces->begin(), pieces->end());
}
void findBestMove(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end)
{
Rating bestRating(grid.width);
findBestMove(grid, start, end, &bestRating);
}
void findBestMove(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end, Rating* bestRating)
{
grid.cleanupFullLines();
Rating curRating(grid.width);
Rating deeperRating(grid.width);
Piece piece = *start;
// for every rotation of the piece
for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++)
{
// put piece to top left corner
piece.x = 0;
piece.y = 0;
//test for every column
for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++)
{
//todo optimise by the use of the previous grids height
piece.landingY = 0;
//will set landingY to final position
grid.findLandingPosition(&piece);
// draw piece
grid.placePiece(&piece, piece.x, piece.landingY);
if(start == end - 1)
{
//at the deepest level
updateRating(grid, &curRating);
}
else
{
//go deeper to take another piece into account
findBestMove(grid, start + 1, end, &deeperRating);
curRating = deeperRating;
}
// eraese piece
grid.erasePiece(&piece, piece.x, piece.landingY);
if(findWorstMove)
{
//init rating for worst
if(bestRating->score == -DBL_MAX)
{
bestRating->score = DBL_MAX;
}
// update if we found a worse one
if (bestRating->score > curRating.score)
{
*bestRating = curRating;
(*start) = piece;
}
}
else
{
// update if we found a better one
if (bestRating->score < curRating.score)
{
*bestRating = curRating;
(*start) = piece;
}
}
}
}
}
bool findBestMoveNonBlocking(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end, Rating* bestRating)
{
//vector with pieces
//for every piece
//for every
switch (expression)
{
case INIT:
break;
default:
break;
}
}
bool findBestMoveNonBlocking(GridBW grid, std::vector<Piece>::iterator start, std::vector<Piece>::iterator end, Rating* bestRating)
{
//INIT
grid.cleanupFullLines();
Rating curRating(grid.width);
Rating deeperRating(grid.width);
Piece piece = *start;
// for every rotation of the piece
piece.rotation = 0;
//HANDLE
while (piece.rotation < piece.pieceData->rotCount)
{
// put piece to top left corner
piece.x = 0;
piece.y = 0;
//test for every column
piece.x = 0;
while (piece.x <= grid.width - piece.getRotation().width)
{
//todo optimise by the use of the previous grids height
piece.landingY = 0;
//will set landingY to final position
grid.findLandingPosition(&piece);
// draw piece
grid.placePiece(&piece, piece.x, piece.landingY);
if(start == end - 1)
{
//at the deepest level
updateRating(grid, &curRating);
}
else
{
//go deeper to take another piece into account
findBestMove(grid, start + 1, end, &deeperRating);
curRating = deeperRating;
}
// eraese piece
grid.erasePiece(&piece, piece.x, piece.landingY);
if(findWorstMove)
{
//init rating for worst
if(bestRating->score == -DBL_MAX)
{
bestRating->score = DBL_MAX;
}
// update if we found a worse one
if (bestRating->score > curRating.score)
{
*bestRating = curRating;
(*start) = piece;
}
}
else
{
// update if we found a better one
if (bestRating->score < curRating.score)
{
*bestRating = curRating;
(*start) = piece;
}
}
piece.x++;
}
piece.rotation++;
}
//EXIT
return true;
}
};
#endif /* __AI_H__ */

View File

@ -0,0 +1,150 @@
/******************************************************************************
* @file : tetrisaigame.h
* @brief : main tetris functions
******************************************************************************
* @attention
*
* Copyright (c) muebau 2022
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __TETRISAIGAME_H__
#define __TETRISAIGAME_H__
#include <stdint.h>
#include <stdbool.h>
#include <vector>
#include "pieces.h"
#include "gridcolor.h"
#include "tetrisbag.h"
#include "tetrisai.h"
using namespace std;
class TetrisAIGame
{
private:
bool animateFallOfPiece(Piece* piece, bool skip)
{
if (skip || piece->y >= piece->landingY)
{
piece->y = piece->landingY;
grid.gridBW.placePiece(piece, piece->x, piece->landingY);
grid.placePiece(piece, piece->x, piece->y);
return false;
}
else
{
// eraese last drawing
grid.erasePiece(piece, piece->x, piece->y);
//move piece down
piece->y++;
// draw piece
grid.placePiece(piece, piece->x, piece->y);
return true;
}
}
public:
uint8_t width;
uint8_t height;
uint8_t nLookAhead;
TetrisBag bag;
GridColor grid;
TetrisAI ai;
Piece curPiece;
PieceData* piecesData;
enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT;
TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces):
width(width),
height(height),
nLookAhead(nLookAhead),
bag(nPieces, 1, nLookAhead),
grid(width, height + 4),
ai(),
piecesData(piecesData)
{
}
void nextPiece()
{
grid.cleanupFullLines();
bag.queuePiece();
}
void findBestMove()
{
ai.findBestMove(grid.gridBW, &bag.piecesQueue);
}
bool animateFall(bool skip)
{
return animateFallOfPiece(&(bag.piecesQueue[0]), skip);
}
bool isGameOver()
{
//if there is something in the 4 lines of the hidden area the game is over
return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3];
}
void poll()
{
switch (state)
{
case INIT:
reset();
state = TEST_GAME_OVER;
break;
case TEST_GAME_OVER:
if (isGameOver())
{
state = ANIMATE_GAME_OVER;
}
else
{
state = GET_NEXT_PIECE;
}
break;
case GET_NEXT_PIECE:
nextPiece();
state = FIND_BEST_MOVE;
break;
case FIND_BEST_MOVE:
findBestMove();
state = ANIMATE_MOVE;
break;
case ANIMATE_MOVE:
if (!animateFall(false))
{
state = TEST_GAME_OVER;
}
break;
case ANIMATE_GAME_OVER:
static auto curPixel = grid.pixels.size();
grid.pixels[curPixel] = 254;
if (curPixel == 0)
{
state = INIT;
curPixel = grid.pixels.size();
}
curPixel--;
break;
}
}
void reset()
{
grid.clear();
bag.init();
}
};
#endif /* __TETRISAIGAME_H__ */

View File

@ -0,0 +1,100 @@
/******************************************************************************
* @file : tetrisbag.h
* @brief : the tetris implementation of a random piece generator
******************************************************************************
* @attention
*
* Copyright (c) muebau 2022
* All rights reserved.</center></h2>
*
******************************************************************************
*/
#ifndef __TETRISBAG_H__
#define __TETRISBAG_H__
#include <stdint.h>
#include <vector>
#include <algorithm>
#include "tetrisbag.h"
class TetrisBag
{
private:
public:
uint8_t nPieces;
uint8_t nBagLength;
uint8_t bagIdx;
std::vector<uint8_t> bag;
std::vector<Piece> piecesQueue;
TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength):
nPieces(nPieces),
nBagLength(nBagLength),
bag(nPieces * nBagLength),
piecesQueue(queueLength)
{
init();
}
void init()
{
//will shuffle the bag at first use
bagIdx = nPieces - 1;
for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++)
{
bag[bagIndex] = bagIndex % nPieces;
}
//will init the queue
for (uint8_t index = 0; index < piecesQueue.size(); index++)
{
queuePiece();
}
}
void shuffleBag()
{
uint8_t temp;
uint8_t swapIdx;
for (int index = nPieces - 1; index > 0; index--)
{
//get candidate to swap
swapIdx = rand() % index;
//swap it!
temp = bag[swapIdx];
bag[swapIdx] = bag[index];
bag[index] = temp;
}
}
Piece getNextPiece()
{
bagIdx++;
if (bagIdx >= nPieces)
{
shuffleBag();
bagIdx = 0;
}
return Piece(bag[bagIdx]);
}
void queuePiece()
{
//move vector to left
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
piecesQueue[piecesQueue.size() - 1] = getNextPiece();
}
void queuePiece(uint8_t idx)
{
//move vector to left
std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end());
piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces);
}
};
#endif /* __TETRISBAG_H__ */

View File

@ -0,0 +1,222 @@
#pragma once
#include "wled.h"
#include "FX.h"
#include "fcn_declare.h"
#include "tetrisaigame.h"
// By: muebau
typedef struct TetrisAI_data
{
unsigned long lastTime = 0;
TetrisAIGame tetris;
uint8_t intelligence;
uint8_t rotate;
bool showNext;
bool showBorder;
uint8_t colorOffset;
uint8_t colorInc;
uint8_t mistaceCountdown;
} tetrisai_data;
void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data)
{
SEGMENT.fill(SEGCOLOR(1));
//GRID
for (auto index_y = 4; index_y < tetris->grid.height; index_y++)
{
for (auto index_x = 0; index_x < tetris->grid.width; index_x++)
{
CRGB color;
if (*tetris->grid.getPixel(index_x, index_y) == 0)
{
//BG color
color = SEGCOLOR(1);
}
//game over animation
else if(*tetris->grid.getPixel(index_x, index_y) == 254)
{
//use fg
color = SEGCOLOR(0);
}
else
{
//spread the color over the whole palette
uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32;
colorIndex += tetrisai_data->colorOffset;
color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND);
}
SEGMENT.setPixelColorXY(index_x, index_y - 4, color);
}
}
tetrisai_data->colorOffset += tetrisai_data->colorInc;
//NEXT PIECE AREA
if (tetrisai_data->showNext)
{
//BORDER
if (tetrisai_data->showBorder)
{
//draw a line 6 pixels from right with the border color
for (auto index_y = 0; index_y < SEGMENT.virtualHeight(); index_y++)
{
SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() - 6, index_y, SEGCOLOR(2));
}
}
//NEXT PIECE
int piecesOffsetX = SEGMENT.virtualWidth() - 4;
int piecesOffsetY = 1;
for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++)
{
uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5;
Piece piece(tetris->bag.piecesQueue[nextPieceIdx]);
for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++)
{
for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++)
{
if (piece.getPixel(pieceX, pieceY))
{
uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset);
SEGMENT.setPixelColorXY(piecesOffsetX + pieceX, piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND));
}
}
}
}
}
}
////////////////////////////
// 2D Tetris AI //
////////////////////////////
uint16_t mode_2DTetrisAI()
{
if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data)))
{
// not a 2D set-up
SEGMENT.fill(SEGCOLOR(0));
return 350;
}
TetrisAI_data* tetrisai_data = reinterpret_cast<TetrisAI_data*>(SEGENV.data);
const uint16_t cols = SEGMENT.virtualWidth();
const uint16_t rows = SEGMENT.virtualHeight();
//range 0 - 1024ms => 1024/255 ~ 4
uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed);
int16_t msDelayGameOver = msDelayMove / 4;
//range 0 - 2 (not including current)
uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1;
//range 0 - 16
tetrisai_data->colorInc = SEGMENT.custom2 >> 4;
if (!tetrisai_data->tetris || (tetrisai_data->tetris.nLookAhead != nLookAhead
|| tetrisai_data->showNext != SEGMENT.check1
|| tetrisai_data->showBorder != SEGMENT.check2
)
)
{
tetrisai_data->showNext = SEGMENT.check1;
tetrisai_data->showBorder = SEGMENT.check2;
//not more than 32 as this is the limit of this implementation
uint8_t gridWidth = cols < 32 ? cols : 32;
uint8_t gridHeight = rows;
// do we need space for the 'next' section?
if (tetrisai_data->showNext)
{
// make space for the piece and one pixel of space
gridWidth = gridWidth - 5;
// do we need space for a border?
if (tetrisai_data->showBorder)
{
gridWidth = gridWidth - 1;
}
}
tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces);
SEGMENT.fill(SEGCOLOR(1));
}
if (tetrisai_data->intelligence != SEGMENT.custom1)
{
tetrisai_data->intelligence = SEGMENT.custom1;
double dui = 0.2 - (0.2 * (tetrisai_data->intelligence / 255.0));
tetrisai_data->tetris.ai.aHeight = -0.510066 + dui;
tetrisai_data->tetris.ai.fullLines = 0.760666 - dui;
tetrisai_data->tetris.ai.holes = -0.35663 + dui;
tetrisai_data->tetris.ai.bumpiness = -0.184483 + dui;
}
if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE)
{
if (millis() - tetrisai_data->lastTime > msDelayMove)
{
drawGrid(&tetrisai_data->tetris, tetrisai_data);
tetrisai_data->lastTime = millis();
tetrisai_data->tetris.poll();
}
}
else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER)
{
if (millis() - tetrisai_data->lastTime > msDelayGameOver)
{
drawGrid(&tetrisai_data->tetris, tetrisai_data);
tetrisai_data->lastTime = millis();
tetrisai_data->tetris.poll();
}
}
else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE)
{
if (SEGMENT.check3)
{
if(tetrisai_data->mistaceCountdown == 0)
{
tetrisai_data->tetris.ai.findWorstMove = true;
tetrisai_data->tetris.poll();
tetrisai_data->tetris.ai.findWorstMove = false;
tetrisai_data->mistaceCountdown = SEGMENT.custom3;
}
tetrisai_data->mistaceCountdown--;
}
tetrisai_data->tetris.poll();
}
else
{
tetrisai_data->tetris.poll();
}
return FRAMETIME;
} // mode_2DTetrisAI()
static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11";
class TetrisAIUsermod : public Usermod
{
private:
public:
void setup()
{
strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI);
}
void loop()
{
}
uint16_t getId()
{
return USERMOD_ID_TETRISAI;
}
};

View File

@ -21,7 +21,6 @@ void shortPressAction(uint8_t b)
case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break; case 1: ++effectCurrent %= strip.getModeCount(); stateChanged = true; colorUpdated(CALL_MODE_BUTTON); break;
} }
} else { } else {
unloadPlaylist(); // applying a preset unloads the playlist
applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET); applyPreset(macroButton[b], CALL_MODE_BUTTON_PRESET);
} }
@ -43,7 +42,6 @@ void longPressAction(uint8_t b)
case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action case 1: bri += 8; stateUpdated(CALL_MODE_BUTTON); buttonPressedTime[b] = millis(); break; // repeatable action
} }
} else { } else {
unloadPlaylist(); // applying a preset unloads the playlist
applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET); applyPreset(macroLongPress[b], CALL_MODE_BUTTON_PRESET);
} }
@ -65,7 +63,6 @@ void doublePressAction(uint8_t b)
case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break;
} }
} else { } else {
unloadPlaylist(); // applying a preset unloads the playlist
applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET);
} }

View File

@ -180,6 +180,7 @@
#define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h"
#define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h"
#define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h" #define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h"
#define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h"
//Access point behavior //Access point behavior
#define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot

View File

@ -11,6 +11,7 @@
//DDP protocol support, called by handleE131Packet //DDP protocol support, called by handleE131Packet
//handles RGB data only //handles RGB data only
void handleDDPPacket(e131_packet_t* p) { void handleDDPPacket(e131_packet_t* p) {
static bool ddpSeenPush = false; // have we seen a push yet?
int lastPushSeq = e131LastSequenceNumber[0]; int lastPushSeq = e131LastSequenceNumber[0];
//reject late packets belonging to previous frame (assuming 4 packets max. before push) //reject late packets belonging to previous frame (assuming 4 packets max. before push)
@ -34,6 +35,7 @@ void handleDDPPacket(e131_packet_t* p) {
uint16_t c = 0; uint16_t c = 0;
if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later
if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet
realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP);
if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) {
@ -44,7 +46,8 @@ void handleDDPPacket(e131_packet_t* p) {
} }
bool push = p->flags & DDP_PUSH_FLAG; bool push = p->flags & DDP_PUSH_FLAG;
if (push) { ddpSeenPush |= push;
if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display
e131NewData = true; e131NewData = true;
byte sn = p->sequenceNum & 0xF; byte sn = p->sequenceNum & 0xF;
if (sn) e131LastSequenceNumber[0] = sn; if (sn) e131LastSequenceNumber[0] = sn;
@ -184,7 +187,6 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){
// only apply preset if not in playlist, or playlist changed // only apply preset if not in playlist, or playlist changed
(currentPlaylist < 0 || dmxValPreset != currentPlaylist)) { (currentPlaylist < 0 || dmxValPreset != currentPlaylist)) {
presetCycCurr = dmxValPreset; presetCycCurr = dmxValPreset;
unloadPlaylist(); // applying a preset unloads the playlist
applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION); applyPreset(dmxValPreset, CALL_MODE_NOTIFICATION);
} }

View File

@ -434,7 +434,6 @@ void handleSerial();
void updateBaudRate(uint32_t rate); void updateBaudRate(uint32_t rate);
//wled_server.cpp //wled_server.cpp
String getFileContentType(String &filename);
void createEditHandler(bool enable); void createEditHandler(bool enable);
void initServer(); void initServer();
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255); void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);

View File

@ -375,6 +375,7 @@ void updateFSInfo() {
#endif #endif
} }
#if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM)
// caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED // caching presets in PSRAM may prevent occasional flashes seen when HomeAssitant polls WLED
// original idea by @akaricchi (https://github.com/Akaricchi) // original idea by @akaricchi (https://github.com/Akaricchi)
@ -420,26 +421,19 @@ bool handleFileRead(AsyncWebServerRequest* request, String path){
DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path);
if(path.endsWith("/")) path += "index.htm"; if(path.endsWith("/")) path += "index.htm";
if(path.indexOf(F("sec")) > -1) return false; if(path.indexOf(F("sec")) > -1) return false;
String contentType = getFileContentType(path);
if(request->hasArg(F("download"))) contentType = F("application/octet-stream");
/*String pathWithGz = path + ".gz";
if(WLED_FS.exists(pathWithGz)){
request->send(WLED_FS, pathWithGz, contentType);
return true;
}*/
#if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM)
if (path.endsWith(FPSTR(getPresetsFileName()))) { if (path.endsWith(FPSTR(getPresetsFileName()))) {
size_t psize; size_t psize;
const uint8_t *presets = getPresetCache(psize); const uint8_t *presets = getPresetCache(psize);
if (presets) { if (presets) {
AsyncWebServerResponse *response = request->beginResponse_P(200, contentType, presets, psize); AsyncWebServerResponse *response = request->beginResponse_P(200, FPSTR(CONTENT_TYPE_JSON), presets, psize);
request->send(response); request->send(response);
return true; return true;
} }
} }
#endif #endif
if(WLED_FS.exists(path)) { if(WLED_FS.exists(path) || WLED_FS.exists(path + ".gz")) {
request->send(WLED_FS, path, contentType); request->send(WLED_FS, path, String(), request->hasArg(F("download")));
return true; return true;
} }
return false; return false;

View File

@ -442,13 +442,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
currentPreset = root[F("pd")] | currentPreset; currentPreset = root[F("pd")] | currentPreset;
if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp] if (root["win"].isNull()) presetCycCurr = currentPreset; // otherwise it was set in handleSet() [set.cpp]
presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after presetToRestore = currentPreset; // stateUpdated() will clear the preset, so we need to restore it after
//unloadPlaylist(); // applying a preset unloads the playlist, may be needed here too?
} else if (!root["ps"].isNull()) { } else if (!root["ps"].isNull()) {
ps = presetCycCurr; ps = presetCycCurr;
if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) { if (root["win"].isNull() && getVal(root["ps"], &ps, 0, 0) && ps > 0 && ps < 251 && ps != currentPreset) {
// b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal())
presetCycCurr = ps; presetCycCurr = ps;
unloadPlaylist(); // applying a preset unloads the playlist
applyPreset(ps, callMode); // async load from file system (only preset ID was specified) applyPreset(ps, callMode); // async load from file system (only preset ID was specified)
return stateResponse; return stateResponse;
} }
@ -1065,7 +1063,7 @@ void serveJson(AsyncWebServerRequest* request)
} }
#endif #endif
else if (url.indexOf("pal") > 0) { else if (url.indexOf("pal") > 0) {
request->send_P(200, "application/json", JSON_palette_names); // contentType defined in AsyncJson-v6.h request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names);
return; return;
} }
else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) {
@ -1152,10 +1150,10 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)
} }
#endif #endif
char buffer[2048]; // shoud be enough for 256 LEDs [RRGGBB] + all other text (9+25) DynamicBuffer buffer(9 + (9*(1+(used/n))) + 7 + 5 + 6 + 5 + 6 + 5 + 2);
strcpy_P(buffer, PSTR("{\"leds\":[")); char* buf = buffer.data(); // assign buffer for oappnd() functions
obuf = buffer; // assign buffer for oappnd() functions strncpy_P(buffer.data(), PSTR("{\"leds\":["), buffer.size());
olen = 9; buf += 9; // sizeof(PSTR()) from last line
for (size_t i = 0; i < used; i += n) for (size_t i = 0; i < used; i += n)
{ {
@ -1170,29 +1168,27 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient)
r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map r = scale8(qadd8(w, r), strip.getBrightness()); //R, add white channel to RGB channels as a simple RGBW -> RGB map
g = scale8(qadd8(w, g), strip.getBrightness()); //G g = scale8(qadd8(w, g), strip.getBrightness()); //G
b = scale8(qadd8(w, b), strip.getBrightness()); //B b = scale8(qadd8(w, b), strip.getBrightness()); //B
olen += sprintf_P(obuf + olen, PSTR("\"%06X\","), RGBW32(r,g,b,0)); buf += sprintf_P(buf, PSTR("\"%06X\","), RGBW32(r,g,b,0));
} }
olen -= 1; buf--; // remove last comma
oappend((const char*)F("],\"n\":")); buf += sprintf_P(buf, PSTR("],\"n\":%d"), n);
oappendi(n);
#ifndef WLED_DISABLE_2D #ifndef WLED_DISABLE_2D
if (strip.isMatrix) { if (strip.isMatrix) {
oappend((const char*)F(",\"w\":")); buf += sprintf_P(buf, PSTR(",\"w\":%d"), Segment::maxWidth/n);
oappendi(Segment::maxWidth/n); buf += sprintf_P(buf, PSTR(",\"h\":%d"), Segment::maxHeight/n);
oappend((const char*)F(",\"h\":"));
oappendi(Segment::maxHeight/n);
} }
#endif #endif
oappend("}"); (*buf++) = '}';
(*buf++) = 0;
if (request) { if (request) {
request->send(200, "application/json", buffer); // contentType defined in AsyncJson-v6.h request->send(200, FPSTR(CONTENT_TYPE_JSON), toString(std::move(buffer)));
} }
#ifdef WLED_ENABLE_WEBSOCKETS #ifdef WLED_ENABLE_WEBSOCKETS
else { else {
wsc->text(obuf, olen); wsc->text(toString(std::move(buffer)));
} }
#endif #endif
obuf = nullptr;
return true; return true;
} }
#endif #endif

View File

@ -172,7 +172,9 @@ void updateInterfaces(uint8_t callMode)
espalexaDevice->setColor(col[0], col[1], col[2]); espalexaDevice->setColor(col[0], col[1], col[2]);
} }
#endif #endif
doPublishMqtt = true; #ifndef WLED_DISABLE_MQTT
publishMqtt();
#endif
} }
@ -180,9 +182,6 @@ void handleTransitions()
{ {
//handle still pending interface update //handle still pending interface update
updateInterfaces(interfaceUpdateCallMode); updateInterfaces(interfaceUpdateCallMode);
#ifndef WLED_DISABLE_MQTT
if (doPublishMqtt) publishMqtt();
#endif
if (transitionActive && strip.getTransition() > 0) { if (transitionActive && strip.getTransition() > 0) {
float tper = (millis() - transitionStartTime)/(float)strip.getTransition(); float tper = (millis() - transitionStartTime)/(float)strip.getTransition();

View File

@ -1,198 +1,197 @@
#include "wled.h" #include "wled.h"
/* /*
* MQTT communication protocol for home automation * MQTT communication protocol for home automation
*/ */
#ifdef WLED_ENABLE_MQTT #ifdef WLED_ENABLE_MQTT
#define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds
void parseMQTTBriPayload(char* payload) void parseMQTTBriPayload(char* payload)
{ {
if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);}
else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);}
else { else {
uint8_t in = strtoul(payload, NULL, 10); uint8_t in = strtoul(payload, NULL, 10);
if (in == 0 && bri > 0) briLast = bri; if (in == 0 && bri > 0) briLast = bri;
bri = in; bri = in;
stateUpdated(CALL_MODE_DIRECT_CHANGE); stateUpdated(CALL_MODE_DIRECT_CHANGE);
} }
} }
void onMqttConnect(bool sessionPresent) void onMqttConnect(bool sessionPresent)
{ {
//(re)subscribe to required topics //(re)subscribe to required topics
char subuf[38]; char subuf[38];
if (mqttDeviceTopic[0] != 0) { if (mqttDeviceTopic[0] != 0) {
strlcpy(subuf, mqttDeviceTopic, 33); strlcpy(subuf, mqttDeviceTopic, 33);
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcat_P(subuf, PSTR("/col")); strcat_P(subuf, PSTR("/col"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strlcpy(subuf, mqttDeviceTopic, 33); strlcpy(subuf, mqttDeviceTopic, 33);
strcat_P(subuf, PSTR("/api")); strcat_P(subuf, PSTR("/api"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
} }
if (mqttGroupTopic[0] != 0) { if (mqttGroupTopic[0] != 0) {
strlcpy(subuf, mqttGroupTopic, 33); strlcpy(subuf, mqttGroupTopic, 33);
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strcat_P(subuf, PSTR("/col")); strcat_P(subuf, PSTR("/col"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
strlcpy(subuf, mqttGroupTopic, 33); strlcpy(subuf, mqttGroupTopic, 33);
strcat_P(subuf, PSTR("/api")); strcat_P(subuf, PSTR("/api"));
mqtt->subscribe(subuf, 0); mqtt->subscribe(subuf, 0);
} }
usermods.onMqttConnect(sessionPresent); usermods.onMqttConnect(sessionPresent);
doPublishMqtt = true; DEBUG_PRINTLN(F("MQTT ready"));
DEBUG_PRINTLN(F("MQTT ready")); publishMqtt();
} }
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
static char *payloadStr; static char *payloadStr;
DEBUG_PRINT(F("MQTT msg: ")); DEBUG_PRINT(F("MQTT msg: "));
DEBUG_PRINTLN(topic); DEBUG_PRINTLN(topic);
// paranoia check to avoid npe if no payload // paranoia check to avoid npe if no payload
if (payload==nullptr) { if (payload==nullptr) {
DEBUG_PRINTLN(F("no payload -> leave")); DEBUG_PRINTLN(F("no payload -> leave"));
return; return;
} }
if (index == 0) { // start (1st partial packet or the only packet) if (index == 0) { // start (1st partial packet or the only packet)
if (payloadStr) delete[] payloadStr; // fail-safe: release buffer if (payloadStr) delete[] payloadStr; // fail-safe: release buffer
payloadStr = new char[total+1]; // allocate new buffer payloadStr = new char[total+1]; // allocate new buffer
} }
if (payloadStr == nullptr) return; // buffer not allocated if (payloadStr == nullptr) return; // buffer not allocated
// copy (partial) packet to buffer and 0-terminate it if it is last packet // copy (partial) packet to buffer and 0-terminate it if it is last packet
char* buff = payloadStr + index; char* buff = payloadStr + index;
memcpy(buff, payload, len); memcpy(buff, payload, len);
if (index + len >= total) { // at end if (index + len >= total) { // at end
payloadStr[total] = '\0'; // terminate c style string payloadStr[total] = '\0'; // terminate c style string
} else { } else {
DEBUG_PRINTLN(F("Partial packet received.")); DEBUG_PRINTLN(F("MQTT partial packet received."));
return; // process next packet return; // process next packet
} }
DEBUG_PRINTLN(payloadStr); DEBUG_PRINTLN(payloadStr);
size_t topicPrefixLen = strlen(mqttDeviceTopic); size_t topicPrefixLen = strlen(mqttDeviceTopic);
if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) { if (strncmp(topic, mqttDeviceTopic, topicPrefixLen) == 0) {
topic += topicPrefixLen; topic += topicPrefixLen;
} else { } else {
topicPrefixLen = strlen(mqttGroupTopic); topicPrefixLen = strlen(mqttGroupTopic);
if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) { if (strncmp(topic, mqttGroupTopic, topicPrefixLen) == 0) {
topic += topicPrefixLen; topic += topicPrefixLen;
} else { } else {
// Non-Wled Topic used here. Probably a usermod subscribed to this topic. // Non-Wled Topic used here. Probably a usermod subscribed to this topic.
usermods.onMqttMessage(topic, payloadStr); usermods.onMqttMessage(topic, payloadStr);
delete[] payloadStr; delete[] payloadStr;
payloadStr = nullptr; payloadStr = nullptr;
return; return;
} }
} }
//Prefix is stripped from the topic at this point //Prefix is stripped from the topic at this point
if (strcmp_P(topic, PSTR("/col")) == 0) { if (strcmp_P(topic, PSTR("/col")) == 0) {
colorFromDecOrHexString(col, payloadStr); colorFromDecOrHexString(col, payloadStr);
colorUpdated(CALL_MODE_DIRECT_CHANGE); colorUpdated(CALL_MODE_DIRECT_CHANGE);
} else if (strcmp_P(topic, PSTR("/api")) == 0) { } else if (strcmp_P(topic, PSTR("/api")) == 0) {
if (!requestJSONBufferLock(15)) { if (!requestJSONBufferLock(15)) {
delete[] payloadStr; delete[] payloadStr;
payloadStr = nullptr; payloadStr = nullptr;
return; return;
} }
if (payloadStr[0] == '{') { //JSON API if (payloadStr[0] == '{') { //JSON API
deserializeJson(*pDoc, payloadStr); deserializeJson(*pDoc, payloadStr);
deserializeState(pDoc->as<JsonObject>()); deserializeState(pDoc->as<JsonObject>());
} else { //HTTP API } else { //HTTP API
String apireq = "win"; apireq += '&'; // reduce flash string usage String apireq = "win"; apireq += '&'; // reduce flash string usage
apireq += payloadStr; apireq += payloadStr;
handleSet(nullptr, apireq); handleSet(nullptr, apireq);
} }
releaseJSONBufferLock(); releaseJSONBufferLock();
} else if (strlen(topic) != 0) { } else if (strlen(topic) != 0) {
// non standard topic, check with usermods // non standard topic, check with usermods
usermods.onMqttMessage(topic, payloadStr); usermods.onMqttMessage(topic, payloadStr);
} else { } else {
// topmost topic (just wled/MAC) // topmost topic (just wled/MAC)
parseMQTTBriPayload(payloadStr); parseMQTTBriPayload(payloadStr);
} }
delete[] payloadStr; delete[] payloadStr;
payloadStr = nullptr; payloadStr = nullptr;
} }
void publishMqtt() void publishMqtt()
{ {
doPublishMqtt = false; if (!WLED_MQTT_CONNECTED) return;
if (!WLED_MQTT_CONNECTED) return; DEBUG_PRINTLN(F("Publish MQTT"));
DEBUG_PRINTLN(F("Publish MQTT"));
#ifndef USERMOD_SMARTNEST
#ifndef USERMOD_SMARTNEST char s[10];
char s[10]; char subuf[48];
char subuf[38];
sprintf_P(s, PSTR("%u"), bri);
sprintf_P(s, PSTR("%u"), bri); strlcpy(subuf, mqttDeviceTopic, 33);
strlcpy(subuf, mqttDeviceTopic, 33); strcat_P(subuf, PSTR("/g"));
strcat_P(subuf, PSTR("/g")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263)
mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263)
sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2]));
sprintf_P(s, PSTR("#%06X"), (col[3] << 24) | (col[0] << 16) | (col[1] << 8) | (col[2])); strlcpy(subuf, mqttDeviceTopic, 33);
strlcpy(subuf, mqttDeviceTopic, 33); strcat_P(subuf, PSTR("/c"));
strcat_P(subuf, PSTR("/c")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263)
mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263)
strlcpy(subuf, mqttDeviceTopic, 33);
strlcpy(subuf, mqttDeviceTopic, 33); strcat_P(subuf, PSTR("/status"));
strcat_P(subuf, PSTR("/status")); mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT
mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT
char apires[1024]; // allocating 1024 bytes from stack can be risky
char apires[1024]; // allocating 1024 bytes from stack can be risky XML_response(nullptr, apires);
XML_response(nullptr, apires); strlcpy(subuf, mqttDeviceTopic, 33);
strlcpy(subuf, mqttDeviceTopic, 33); strcat_P(subuf, PSTR("/v"));
strcat_P(subuf, PSTR("/v")); mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263)
mqtt->publish(subuf, 0, retainMqttMsg, apires); // optionally retain message (#2263) #endif
#endif }
}
//HA autodiscovery was removed in favor of the native integration in HA v0.102.0
//HA autodiscovery was removed in favor of the native integration in HA v0.102.0
bool initMqtt()
bool initMqtt() {
{ if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false;
if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false;
if (mqtt == nullptr) {
if (mqtt == nullptr) { mqtt = new AsyncMqttClient();
mqtt = new AsyncMqttClient(); mqtt->onMessage(onMqttMessage);
mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect);
mqtt->onConnect(onMqttConnect); }
} if (mqtt->connected()) return true;
if (mqtt->connected()) return true;
DEBUG_PRINTLN(F("Reconnecting MQTT"));
DEBUG_PRINTLN(F("Reconnecting MQTT")); IPAddress mqttIP;
IPAddress mqttIP; if (mqttIP.fromString(mqttServer)) //see if server is IP or domain
if (mqttIP.fromString(mqttServer)) //see if server is IP or domain {
{ mqtt->setServer(mqttIP, mqttPort);
mqtt->setServer(mqttIP, mqttPort); } else {
} else { mqtt->setServer(mqttServer, mqttPort);
mqtt->setServer(mqttServer, mqttPort); }
} mqtt->setClientId(mqttClientID);
mqtt->setClientId(mqttClientID); if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass);
if (mqttUser[0] && mqttPass[0]) mqtt->setCredentials(mqttUser, mqttPass);
#ifndef USERMOD_SMARTNEST
#ifndef USERMOD_SMARTNEST strlcpy(mqttStatusTopic, mqttDeviceTopic, 33);
strlcpy(mqttStatusTopic, mqttDeviceTopic, 33); strcat_P(mqttStatusTopic, PSTR("/status"));
strcat_P(mqttStatusTopic, PSTR("/status")); mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message
mqtt->setWill(mqttStatusTopic, 0, true, "offline"); // LWT message #endif
#endif mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME);
mqtt->setKeepAlive(MQTT_KEEP_ALIVE_TIME); mqtt->connect();
mqtt->connect(); return true;
return true; }
} #endif
#endif

View File

@ -399,7 +399,6 @@ void checkTimers()
&& isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i]) && isTodayInDateRange(((timerMonth[i] >> 4) & 0x0F), timerDay[i], timerMonth[i] & 0x0F, timerDayEnd[i])
) )
{ {
unloadPlaylist();
applyPreset(timerMacro[i]); applyPreset(timerMacro[i]);
} }
} }
@ -413,7 +412,6 @@ void checkTimers()
&& (timerWeekday[8] & 0x01) //timer is enabled && (timerWeekday[8] & 0x01) //timer is enabled
&& ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week && ((timerWeekday[8] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week
{ {
unloadPlaylist();
applyPreset(timerMacro[8]); applyPreset(timerMacro[8]);
DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]); DEBUG_PRINTF_P(PSTR("Sunrise macro %d triggered."),timerMacro[8]);
} }
@ -428,7 +426,6 @@ void checkTimers()
&& (timerWeekday[9] & 0x01) //timer is enabled && (timerWeekday[9] & 0x01) //timer is enabled
&& ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week && ((timerWeekday[9] >> weekdayMondayFirst()) & 0x01)) //timer should activate at current day of week
{ {
unloadPlaylist();
applyPreset(timerMacro[9]); applyPreset(timerMacro[9]);
DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]); DEBUG_PRINTF_P(PSTR("Sunset macro %d triggered."),timerMacro[9]);
} }

View File

@ -119,6 +119,7 @@ void initPresetsFile()
bool applyPreset(byte index, byte callMode) bool applyPreset(byte index, byte callMode)
{ {
unloadPlaylist(); // applying a preset unloads the playlist (#3827)
DEBUG_PRINT(F("Request to apply preset: ")); DEBUG_PRINT(F("Request to apply preset: "));
DEBUG_PRINTLN(index); DEBUG_PRINTLN(index);
presetToApply = index; presetToApply = index;

View File

@ -108,7 +108,6 @@ static void setOff() {
void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) { void presetWithFallback(uint8_t presetID, uint8_t effectID, uint8_t paletteID) {
resetNightMode(); resetNightMode();
unloadPlaylist();
applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID); applyPresetWithFallback(presetID, CALL_MODE_BUTTON_PRESET, effectID, paletteID);
} }

View File

@ -883,7 +883,6 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply)
//apply preset //apply preset
if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) {
unloadPlaylist();
applyPreset(presetCycCurr); applyPreset(presetCycCurr);
} }

View File

@ -21,8 +21,6 @@
#define DYNAMIC_JSON_DOCUMENT_SIZE 16384 #define DYNAMIC_JSON_DOCUMENT_SIZE 16384
#endif #endif
constexpr const char* JSON_MIMETYPE = "application/json";
/* /*
* Json Response * Json Response
* */ * */
@ -66,7 +64,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse {
AsyncJsonResponse(JsonDocument *ref, bool isArray=false) : _jsonBuffer(1), _isValid{false} { AsyncJsonResponse(JsonDocument *ref, bool isArray=false) : _jsonBuffer(1), _isValid{false} {
_code = 200; _code = 200;
_contentType = JSON_MIMETYPE; _contentType = FPSTR(CONTENT_TYPE_JSON);
if(isArray) if(isArray)
_root = ref->to<JsonArray>(); _root = ref->to<JsonArray>();
else else
@ -75,7 +73,7 @@ class AsyncJsonResponse: public AsyncAbstractResponse {
AsyncJsonResponse(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE, bool isArray=false) : _jsonBuffer(maxJsonBufferSize), _isValid{false} { AsyncJsonResponse(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE, bool isArray=false) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
_code = 200; _code = 200;
_contentType = JSON_MIMETYPE; _contentType = FPSTR(CONTENT_TYPE_JSON);
if(isArray) if(isArray)
_root = _jsonBuffer.createNestedArray(); _root = _jsonBuffer.createNestedArray();
else else

View File

@ -209,6 +209,10 @@
#include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h"
#endif #endif
#ifdef USERMOD_TETRISAI
#include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h"
#endif
void registerUsermods() void registerUsermods()
{ {
/* /*
@ -405,4 +409,8 @@ void registerUsermods()
#ifdef USERMOD_STAIRCASE_WIPE #ifdef USERMOD_STAIRCASE_WIPE
usermods.add(new StairwayWipeUsermod()); usermods.add(new StairwayWipeUsermod());
#endif #endif
#ifdef USERMOD_TETRISAI
usermods.add(new TetrisAIUsermod());
#endif
} }

View File

@ -269,6 +269,7 @@ void WLED::loop()
maxLoopMillis = 0; maxLoopMillis = 0;
maxUsermodMillis = 0; maxUsermodMillis = 0;
maxStripMillis = 0; maxStripMillis = 0;
avgLoopMillis = 0;
avgUsermodMillis = 0; avgUsermodMillis = 0;
avgStripMillis = 0; avgStripMillis = 0;
debugTime = millis(); debugTime = millis();

3
wled00/wled.h Executable file → Normal file
View File

@ -8,7 +8,7 @@
*/ */
// version code in format yymmddb (b = daily build) // version code in format yymmddb (b = daily build)
#define VERSION 2403100 #define VERSION 2403190
//uncomment this if you have a "my_config.h" file you'd like to use //uncomment this if you have a "my_config.h" file you'd like to use
//#define WLED_USE_MY_CONFIG //#define WLED_USE_MY_CONFIG
@ -705,7 +705,6 @@ WLED_GLOBAL byte optionType;
WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config
WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers
WLED_GLOBAL bool doPublishMqtt _INIT(false);
// status led // status led
#if defined(STATUSLED) #if defined(STATUSLED)

View File

@ -18,36 +18,6 @@ static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security setti
static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!";
static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_notimplemented[] PROGMEM = "Not implemented";
static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char s_accessdenied[] PROGMEM = "Access Denied";
static const char s_javascript[] PROGMEM = "application/javascript";
static const char s_json[] = "application/json"; // AsyncJson-v6.h
static const char s_html[] PROGMEM = "text/html";
static const char s_plain[] = "text/plain"; // Espalexa.h
static const char s_css[] PROGMEM = "text/css";
static const char s_png[] PROGMEM = "image/png";
static const char s_gif[] PROGMEM = "image/gif";
static const char s_jpg[] PROGMEM = "image/jpeg";
static const char s_ico[] PROGMEM = "image/x-icon";
//static const char s_xml[] PROGMEM = "text/xml";
//static const char s_pdf[] PROGMEM = "application/x-pdf";
//static const char s_zip[] PROGMEM = "application/x-zip";
//static const char s_gz[] PROGMEM = "application/x-gzip";
String getFileContentType(String &filename) {
if (filename.endsWith(F(".htm"))) return FPSTR(s_html);
else if (filename.endsWith(F(".html"))) return FPSTR(s_html);
else if (filename.endsWith(F(".css"))) return FPSTR(s_css);
else if (filename.endsWith(F(".js"))) return FPSTR(s_javascript);
else if (filename.endsWith(F(".json"))) return s_json;
else if (filename.endsWith(F(".png"))) return FPSTR(s_png);
else if (filename.endsWith(F(".gif"))) return FPSTR(s_gif);
else if (filename.endsWith(F(".jpg"))) return FPSTR(s_jpg);
else if (filename.endsWith(F(".ico"))) return FPSTR(s_ico);
// else if (filename.endsWith(F(".xml"))) return FPSTR(s_xml);
// else if (filename.endsWith(F(".pdf"))) return FPSTR(s_pdf);
// else if (filename.endsWith(F(".zip"))) return FPSTR(s_zip);
// else if (filename.endsWith(F(".gz"))) return FPSTR(s_gz);
return s_plain;
}
//Is this an IP? //Is this an IP?
static bool isIp(String str) { static bool isIp(String str) {
@ -183,7 +153,7 @@ static String msgProcessor(const String& var)
static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { static void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!correctPIN) { if (!correctPIN) {
if (final) request->send(401, FPSTR(s_plain), FPSTR(s_unlock_cfg)); if (final) request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_unlock_cfg));
return; return;
} }
if (!index) { if (!index) {
@ -204,10 +174,10 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
request->_tempFile.close(); request->_tempFile.close();
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
doReboot = true; doReboot = true;
request->send(200, FPSTR(s_plain), F("Configuration restore successful.\nRebooting...")); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
} else { } else {
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes(); if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes();
request->send(200, FPSTR(s_plain), F("File Uploaded!")); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
} }
cacheInvalidate++; cacheInvalidate++;
} }
@ -259,24 +229,24 @@ void initServer()
#ifdef WLED_ENABLE_WEBSOCKETS #ifdef WLED_ENABLE_WEBSOCKETS
#ifndef WLED_DISABLE_2D #ifndef WLED_DISABLE_2D
server.on(SET_F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) { server.on(F("/liveview2D"), HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveviewws2D, PAGE_liveviewws2D_length); handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveviewws2D, PAGE_liveviewws2D_length);
}); });
#endif #endif
#endif #endif
server.on(SET_F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) { server.on(F("/liveview"), HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_liveview, PAGE_liveview_length); handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length);
}); });
//settings page //settings page
server.on(SET_F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){
serveSettings(request); serveSettings(request);
}); });
// "/settings/settings.js&p=x" request also handled by serveSettings() // "/settings/settings.js&p=x" request also handled by serveSettings()
static const char _style_css[] PROGMEM = "/style.css"; static const char _style_css[] PROGMEM = "/style.css";
server.on(_style_css, HTTP_GET, [](AsyncWebServerRequest *request) { server.on(_style_css, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(s_css), PAGE_settingsCss, PAGE_settingsCss_length); handleStaticContent(request, FPSTR(_style_css), 200, FPSTR(CONTENT_TYPE_CSS), PAGE_settingsCss, PAGE_settingsCss_length);
}); });
static const char _favicon_ico[] PROGMEM = "/favicon.ico"; static const char _favicon_ico[] PROGMEM = "/favicon.ico";
@ -287,28 +257,29 @@ void initServer()
static const char _skin_css[] PROGMEM = "/skin.css"; static const char _skin_css[] PROGMEM = "/skin.css";
server.on(_skin_css, HTTP_GET, [](AsyncWebServerRequest *request) { server.on(_skin_css, HTTP_GET, [](AsyncWebServerRequest *request) {
if (handleFileRead(request, FPSTR(_skin_css))) return; if (handleFileRead(request, FPSTR(_skin_css))) return;
AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(s_css)); AsyncWebServerResponse *response = request->beginResponse(200, FPSTR(CONTENT_TYPE_CSS));
request->send(response); request->send(response);
}); });
server.on(SET_F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/welcome"), HTTP_GET, [](AsyncWebServerRequest *request){
serveSettings(request); serveSettings(request);
}); });
server.on(SET_F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129); serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129);
doReboot = true; doReboot = true;
}); });
server.on(SET_F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){ server.on(F("/settings"), HTTP_POST, [](AsyncWebServerRequest *request){
serveSettings(request, true); serveSettings(request, true);
}); });
server.on(SET_F("/json"), HTTP_GET, [](AsyncWebServerRequest *request){ const static char _json[] PROGMEM = "/json";
server.on(FPSTR(_json), HTTP_GET, [](AsyncWebServerRequest *request){
serveJson(request); serveJson(request);
}); });
AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(F("/json"), [](AsyncWebServerRequest *request) { AsyncCallbackJsonWebHandler* handler = new AsyncCallbackJsonWebHandler(FPSTR(_json), [](AsyncWebServerRequest *request) {
bool verboseResponse = false; bool verboseResponse = false;
bool isConfig = false; bool isConfig = false;
@ -356,33 +327,33 @@ void initServer()
doSerializeConfig = true; //serializeConfig(); //Save new settings to FS doSerializeConfig = true; //serializeConfig(); //Save new settings to FS
} }
} }
request->send(200, s_json, F("{\"success\":true}")); request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}"));
}, JSON_BUFFER_SIZE); }, JSON_BUFFER_SIZE);
server.addHandler(handler); server.addHandler(handler);
server.on(SET_F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/version"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, FPSTR(s_plain), (String)VERSION); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)VERSION);
}); });
server.on(SET_F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/uptime"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, FPSTR(s_plain), (String)millis()); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)millis());
}); });
server.on(SET_F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/freeheap"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, FPSTR(s_plain), (String)ESP.getFreeHeap()); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)ESP.getFreeHeap());
}); });
#ifdef WLED_ENABLE_USERMOD_PAGE #ifdef WLED_ENABLE_USERMOD_PAGE
server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, "", 200, FPSTR(s_html), PAGE_usermod, PAGE_usermod_length); handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length);
}); });
#endif #endif
server.on(SET_F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/teapot"), HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254); serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254);
}); });
server.on(SET_F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {}, server.on(F("/upload"), HTTP_POST, [](AsyncWebServerRequest *request) {},
[](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data,
size_t len, bool final) {handleUpload(request, filename, index, data, len, final);} size_t len, bool final) {handleUpload(request, filename, index, data, len, final);}
); );
@ -453,7 +424,7 @@ void initServer()
#ifdef WLED_ENABLE_DMX #ifdef WLED_ENABLE_DMX
server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, FPSTR(s_html), PAGE_dmxmap , dmxProcessor); request->send_P(200, FPSTR(CONTENT_TYPE_HTML), PAGE_dmxmap , dmxProcessor);
}); });
#else #else
server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(SET_F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){
@ -464,7 +435,7 @@ void initServer()
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
if (captivePortal(request)) return; if (captivePortal(request)) return;
if (!showWelcomePage || request->hasArg(F("sliders"))) { if (!showWelcomePage || request->hasArg(F("sliders"))) {
handleStaticContent(request, F("/index.htm"), 200, FPSTR(s_html), PAGE_index, PAGE_index_L); handleStaticContent(request, F("/index.htm"), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_index, PAGE_index_L);
} else { } else {
serveSettings(request); serveSettings(request);
} }
@ -473,20 +444,20 @@ void initServer()
#ifdef WLED_ENABLE_PIXART #ifdef WLED_ENABLE_PIXART
static const char _pixart_htm[] PROGMEM = "/pixart.htm"; static const char _pixart_htm[] PROGMEM = "/pixart.htm";
server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) { server.on(_pixart_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(s_html), PAGE_pixart, PAGE_pixart_L); handleStaticContent(request, FPSTR(_pixart_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pixart, PAGE_pixart_L);
}); });
#endif #endif
#ifndef WLED_DISABLE_PXMAGIC #ifndef WLED_DISABLE_PXMAGIC
static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm"; static const char _pxmagic_htm[] PROGMEM = "/pxmagic.htm";
server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) { server.on(_pxmagic_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(s_html), PAGE_pxmagic, PAGE_pxmagic_L); handleStaticContent(request, FPSTR(_pxmagic_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_pxmagic, PAGE_pxmagic_L);
}); });
#endif #endif
static const char _cpal_htm[] PROGMEM = "/cpal.htm"; static const char _cpal_htm[] PROGMEM = "/cpal.htm";
server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) { server.on(_cpal_htm, HTTP_GET, [](AsyncWebServerRequest *request) {
handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(s_html), PAGE_cpal, PAGE_cpal_L); handleStaticContent(request, FPSTR(_cpal_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_cpal, PAGE_cpal_L);
}); });
#ifdef WLED_ENABLE_WEBSOCKETS #ifdef WLED_ENABLE_WEBSOCKETS
@ -511,7 +482,7 @@ void initServer()
#ifndef WLED_DISABLE_ALEXA #ifndef WLED_DISABLE_ALEXA
if(espalexa.handleAlexaApiCall(request)) return; if(espalexa.handleAlexaApiCall(request)) return;
#endif #endif
handleStaticContent(request, request->url(), 404, FPSTR(s_html), PAGE_404, PAGE_404_length); handleStaticContent(request, request->url(), 404, FPSTR(CONTENT_TYPE_HTML), PAGE_404, PAGE_404_length);
}); });
} }
@ -522,7 +493,7 @@ void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& h
messageSub = subl; messageSub = subl;
optionType = optionT; optionType = optionT;
request->send_P(code, FPSTR(s_html), PAGE_msg, msgProcessor); request->send_P(code, FPSTR(CONTENT_TYPE_HTML), PAGE_msg, msgProcessor);
} }
@ -530,7 +501,7 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro
{ {
AsyncJsonResponse *response = new AsyncJsonResponse(64); AsyncJsonResponse *response = new AsyncJsonResponse(64);
if (error < ERR_NOT_IMPL) response->addHeader(F("Retry-After"), F("1")); if (error < ERR_NOT_IMPL) response->addHeader(F("Retry-After"), F("1"));
response->setContentType(s_json); response->setContentType(CONTENT_TYPE_JSON);
response->setCode(code); response->setCode(code);
JsonObject obj = response->getRoot(); JsonObject obj = response->getRoot();
obj[F("error")] = error; obj[F("error")] = error;
@ -546,12 +517,12 @@ void serveSettingsJS(AsyncWebServerRequest* request)
byte subPage = request->arg(F("p")).toInt(); byte subPage = request->arg(F("p")).toInt();
if (subPage > 10) { if (subPage > 10) {
strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');")); strcpy_P(buf, PSTR("alert('Settings for this request are not implemented.');"));
request->send(501, FPSTR(s_javascript), buf); request->send(501, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf);
return; return;
} }
if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) { if (subPage > 0 && !correctPIN && strlen(settingsPIN)>0) {
strcpy_P(buf, PSTR("alert('PIN incorrect.');")); strcpy_P(buf, PSTR("alert('PIN incorrect.');"));
request->send(401, FPSTR(s_javascript), buf); request->send(401, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf);
return; return;
} }
strcat_P(buf,PSTR("function GetV(){var d=document;")); strcat_P(buf,PSTR("function GetV(){var d=document;"));
@ -559,7 +530,7 @@ void serveSettingsJS(AsyncWebServerRequest* request)
strcat_P(buf,PSTR("}")); strcat_P(buf,PSTR("}"));
AsyncWebServerResponse *response; AsyncWebServerResponse *response;
response = request->beginResponse(200, FPSTR(s_javascript), buf); response = request->beginResponse(200, FPSTR(CONTENT_TYPE_JAVASCRIPT), buf);
response->addHeader(F("Cache-Control"), F("no-store")); response->addHeader(F("Cache-Control"), F("no-store"));
response->addHeader(F("Expires"), F("0")); response->addHeader(F("Expires"), F("0"));
request->send(response); request->send(response);
@ -640,7 +611,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
} }
int code = 200; int code = 200;
String contentType = FPSTR(s_html); String contentType = FPSTR(CONTENT_TYPE_HTML);
const uint8_t* content; const uint8_t* content;
size_t len; size_t len;
@ -666,7 +637,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
return; return;
} }
case SUBPAGE_PINREQ : content = PAGE_settings_pin; len = PAGE_settings_pin_length; code = 401; break; case SUBPAGE_PINREQ : content = PAGE_settings_pin; len = PAGE_settings_pin_length; code = 401; break;
case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(s_css); break; case SUBPAGE_CSS : content = PAGE_settingsCss; len = PAGE_settingsCss_length; contentType = FPSTR(CONTENT_TYPE_CSS); break;
case SUBPAGE_JS : serveSettingsJS(request); return; case SUBPAGE_JS : serveSettingsJS(request); return;
case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break; case SUBPAGE_WELCOME : content = PAGE_welcome; len = PAGE_welcome_length; break;
default: content = PAGE_settings; len = PAGE_settings_length; break; default: content = PAGE_settings; len = PAGE_settings_length; break;

View File

@ -102,7 +102,6 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp
void sendDataWs(AsyncWebSocketClient * client) void sendDataWs(AsyncWebSocketClient * client)
{ {
if (!ws.count()) return; if (!ws.count()) return;
AsyncWebSocketMessageBuffer * buffer;
if (!requestJSONBufferLock(12)) { if (!requestJSONBufferLock(12)) {
if (client) { if (client) {
@ -129,7 +128,7 @@ void sendDataWs(AsyncWebSocketClient * client)
return; return;
} }
#endif #endif
buffer = ws.makeBuffer(len); // will not allocate correct memory sometimes on ESP8266 AsyncWebSocketBuffer buffer(len);
#ifdef ESP8266 #ifdef ESP8266
size_t heap2 = ESP.getFreeHeap(); size_t heap2 = ESP.getFreeHeap();
DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap());
@ -141,23 +140,18 @@ void sendDataWs(AsyncWebSocketClient * client)
DEBUG_PRINTLN(F("WS buffer allocation failed.")); DEBUG_PRINTLN(F("WS buffer allocation failed."));
ws.closeAll(1013); //code 1013 = temporary overload, try again later ws.closeAll(1013); //code 1013 = temporary overload, try again later
ws.cleanupClients(0); //disconnect all clients to release memory ws.cleanupClients(0); //disconnect all clients to release memory
ws._cleanBuffers();
return; //out of memory return; //out of memory
} }
serializeJson(*pDoc, (char *)buffer.data(), len);
buffer->lock();
serializeJson(*pDoc, (char *)buffer->get(), len);
DEBUG_PRINT(F("Sending WS data ")); DEBUG_PRINT(F("Sending WS data "));
if (client) { if (client) {
client->text(buffer); client->text(std::move(buffer));
DEBUG_PRINTLN(F("to a single client.")); DEBUG_PRINTLN(F("to a single client."));
} else { } else {
ws.textAll(buffer); ws.textAll(std::move(buffer));
DEBUG_PRINTLN(F("to multiple clients.")); DEBUG_PRINTLN(F("to multiple clients."));
} }
buffer->unlock();
ws._cleanBuffers();
releaseJSONBufferLock(); releaseJSONBufferLock();
} }
@ -187,11 +181,10 @@ bool sendLiveLedsWs(uint32_t wsClient)
#endif #endif
size_t bufSize = pos + (used/n)*3; size_t bufSize = pos + (used/n)*3;
AsyncWebSocketMessageBuffer * wsBuf = ws.makeBuffer(bufSize); AsyncWebSocketBuffer wsBuf(bufSize);
if (!wsBuf) return false; //out of memory if (!wsBuf) return false; //out of memory
uint8_t* buffer = wsBuf->get(); uint8_t* buffer = reinterpret_cast<uint8_t*>(wsBuf.data());
if (!buffer) return false; //out of memory if (!buffer) return false; //out of memory
wsBuf->lock(); // protect buffer from being cleaned by another WS instance
buffer[0] = 'L'; buffer[0] = 'L';
buffer[1] = 1; //version buffer[1] = 1; //version
@ -218,9 +211,7 @@ bool sendLiveLedsWs(uint32_t wsClient)
buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B buffer[pos++] = scale8(qadd8(w, b), strip.getBrightness()); //B
} }
wsc->binary(wsBuf); wsc->binary(std::move(wsBuf));
wsBuf->unlock(); // un-protect buffer
ws._cleanBuffers();
return true; return true;
} }

View File

@ -471,8 +471,10 @@ void getSettingsJS(byte subPage, char* dest)
} }
sappend('c',SET_F("IP"),disablePullUp); sappend('c',SET_F("IP"),disablePullUp);
sappend('v',SET_F("TT"),touchThreshold); sappend('v',SET_F("TT"),touchThreshold);
#ifndef WLED_DISABLE_INFRARED
sappend('v',SET_F("IR"),irPin); sappend('v',SET_F("IR"),irPin);
sappend('v',SET_F("IT"),irEnabled); sappend('v',SET_F("IT"),irEnabled);
#endif
sappend('c',SET_F("MSO"),!irApplyToAllSelected); sappend('c',SET_F("MSO"),!irApplyToAllSelected);
} }