From 9a87d2bd8e829ee786613ff8f0200e9f5a3c4034 Mon Sep 17 00:00:00 2001 From: Ewowi Date: Mon, 29 Aug 2022 13:56:46 +0200 Subject: [PATCH 1/4] Add CustomEffects as usermod: step 1: c compiles - Only C part, it only compiles - UI to be done - Needs to be tested --- usermods/customeffects/arti.h | 2718 +++++++++++++++++ usermods/customeffects/arti_wled.h | 590 ++++ .../customeffects/usermod_v2_customeffects.h | 199 ++ wled00/FX.h | 8 + wled00/const.h | 1 + wled00/usermods_list.cpp | 9 + 6 files changed, 3525 insertions(+) create mode 100644 usermods/customeffects/arti.h create mode 100644 usermods/customeffects/arti_wled.h create mode 100644 usermods/customeffects/usermod_v2_customeffects.h diff --git a/usermods/customeffects/arti.h b/usermods/customeffects/arti.h new file mode 100644 index 00000000..8953134c --- /dev/null +++ b/usermods/customeffects/arti.h @@ -0,0 +1,2718 @@ +/* + @title Arduino Real Time Interpreter (ARTI) + @file arti.h + @version 0.3.1 + @date 20220818 + @author Ewoud Wijma + @repo https://github.com/ewoudwijma/ARTI + @remarks + - #define ARDUINOJSON_DEFAULT_NESTING_LIMIT 100 //set this in ArduinoJson!!!, currently not necessary... + - IF UPDATING THIS FILE IN THE WLED REPO, SEND A PULL REQUEST TO https://github.com/ewoudwijma/ARTI AS WELL!!! + @later + - Code improvememt + - See 'for some weird reason this causes a crash on esp32' + - check why column/lineno not correct + - Definition improvements + - support string (e.g. for print) + - add integer and string stacks + - print every x seconds (to use it in loops. e.g. to show free memory) + - reserved words (ext functions and variables cannot be used as variables) + - check on return values + - arrays (indices) for varref + - WLED improvements + - rename program to sketch? + @progress + - SetPixelColor without colorwheel + - extend errorOccurred and add warnings (continue) next to errors (stop). Include stack full/empty + - WLED: *arti in SEGENV.data: not working well as change mode will free(data) + - move code from interpreter to analyzer to speed up interpreting + @done? + @done + - save log after first run of loop to get runtime errors included (or 30 iterations to also capture any stack overflows) + @todo + - check why statement is not 'shrinked' + - make default work in js (remove default as we have now load template) + - add PI + - color_fade_pulse because /pixelCount instead of ledCount should not crash + */ + +#pragma once + +#define ARTI_SERIAL 1 +#define ARTI_FILE 2 + +#if ARTI_PLATFORM == ARTI_ARDUINO //defined in arti_definition.h e.g. arti_wled.h! + #include "../../wled00/wled.h" + #include "../../wled00/src/dependencies/json/ArduinoJson-v6.h" + + File logFile; + + #define ARTI_ERRORWARNING 1 //shows lexer, parser, analyzer and interpreter errors + // #define ARTI_DEBUG 1 + // #define ARTI_ANDBG 1 + // #define ARTI_RUNLOG 1 //if set on arduino this will create massive amounts of output (as ran in a loop) + #define ARTI_MEMORY 1 //to do analyses of memory usage, trace memoryleaks (works only on arduino) + #define ARTI_PRINT 1 //will show the printf calls + + const char spaces[51] PROGMEM = " "; + #define FREE_SIZE esp_get_free_heap_size() + // #define OPTIMIZED_TREE 1 +#else //embedded + #include "dependencies/ArduinoJson-recent.h" + + FILE * logFile; // FILE needed to use in fprintf (std stream does not work) + + #define ARTI_ERRORWARNING 1 + #define ARTI_DEBUG 1 + #define ARTI_ANDBG 1 + #define ARTI_RUNLOG 1 + #define ARTI_MEMORY 1 + #define ARTI_PRINT 1 + + #include + #include + #include + #include + + const char spaces[51] = " "; + #define FREE_SIZE (unsigned int)0 + // #define OPTIMIZED_TREE 1 +#endif + +bool logToFile = true; //print output to file (e.g. default.wled.log) +uint32_t frameCounter = 0; //tbd move to class if more instances run + +void artiPrintf(char const * format, ...) +{ + va_list argp; + + va_start(argp, format); + + if (!logToFile) + { + vprintf(format, argp); + } + else + { + #if ARTI_PLATFORM == ARTI_ARDUINO + // rocket science here! As logfile.printf causes crashes/wrong output we create our own printf here + // logFile.printf(format, argp); + for (int i = 0; i < strlen(format); i++) + { + if (format[i] == '%') + { + switch (format[i+1]) + { + case 's': + logFile.print(va_arg(argp, const char *)); + break; + case 'u': + logFile.print(va_arg(argp, unsigned int)); + break; + case 'c': + logFile.print((char)va_arg(argp, int)); + break; + case 'f': + logFile.print(va_arg(argp, double)); + break; + case '%': + logFile.print("%"); // in case of %% + break; + default: + va_arg(argp, int); + // logFile.print(x); + logFile.print(format[i]); + logFile.print(format[i+1]); + } + i++; + } + else + { + logFile.print(format[i]); + } + } + #else + vfprintf(logFile, format, argp); + #endif + } + va_end(argp); +} + +//add millis function for non arduino +#if ARTI_PLATFORM != ARTI_ARDUINO + uint32_t millis() + { + return std::chrono::system_clock::now().time_since_epoch().count(); + } +#endif + +#ifdef ARTI_DEBUG + #define DEBUG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define DEBUG_ARTI(...) +#endif + +#ifdef ARTI_ANDBG + #define ANDBG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define ANDBG_ARTI(...) +#endif + +#ifdef ARTI_RUNLOG + #define RUNLOG_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define RUNLOG_ARTI(...) +#endif + +#ifdef ARTI_PRINT + #define PRINT_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define PRINT_ARTI(...) +#endif + +#ifdef ARTI_ERRORWARNING + #define ERROR_ARTI(...) artiPrintf(__VA_ARGS__) + #define WARNING_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define ERROR_ARTI(...) + #define WARNING_ARTI(...) +#endif + +#ifdef ARTI_MEMORY + #define MEMORY_ARTI(...) artiPrintf(__VA_ARGS__) +#else + #define MEMORY_ARTI(...) +#endif + +#define charLength 30 +#define fileNameLength 50 +#define arrayLength 30 + +#define floatNull -32768 + +const char * stringOrEmpty(const char *charS) { + if (charS == nullptr) + return ""; + else + return charS; +} + +//define strupr as only supported in windows toolchain +char* strupr(char* s) +{ + char* tmp = s; + + for (;*tmp;++tmp) { + *tmp = toupper((unsigned char) *tmp); + } + + return s; +} + +enum Tokens +{ + F_integerConstant, + F_realConstant, + F_plus, + F_minus, + F_multiplication, + F_division, + F_modulo, + F_bitShiftLeft, + F_bitShiftRight, + F_equal, + F_notEqual, + F_lessThen, + F_lessThenOrEqual, + F_greaterThen, + F_greaterThenOrEqual, + F_and, + F_or, + F_plusplus, + F_minmin, + F_NoToken = 255 +}; + +const char * tokenToString(uint8_t key) +{ + switch (key) { + case F_integerConstant: + return "integer"; + break; + case F_realConstant: + return "real"; + break; + case F_plus: + return "+"; + break; + case F_minus: + return "-"; + break; + case F_multiplication: + return "*"; + break; + case F_division: + return "/"; + break; + case F_modulo: + return "%"; + break; + case F_bitShiftLeft: + return "<<"; + break; + case F_bitShiftRight: + return ">>"; + break; + case F_equal: + return "=="; + break; + case F_notEqual: + return "!="; + break; + case F_lessThen: + return "<"; + break; + case F_lessThenOrEqual: + return "<="; + break; + case F_greaterThen: + return ">"; + break; + case F_greaterThenOrEqual: + return ">="; + break; + case F_and: + return "&&"; + break; + case F_or: + return "||"; + break; + case F_plusplus: + return "++"; + break; + case F_minmin: + return "--"; + break; + } + return "unknown key"; +} + +uint8_t stringToToken(const char * token, const char * value) +{ + if (strcmp(token, "INTEGER_CONST") == 0) + return F_integerConstant; + else if (strcmp(token, "REAL_CONST") == 0) + return F_realConstant; + else if (strcmp(value, "+") == 0) + return F_plus; + else if (strcmp(value, "-") == 0) + return F_minus; + else if (strcmp(value, "*") == 0) + return F_multiplication; + else if (strcmp(value, "/") == 0) + return F_division; + else if (strcmp(value, "%") == 0) + return F_modulo; + else if (strcmp(value, "<<") == 0) + return F_bitShiftLeft; + else if (strcmp(value, ">>") == 0) + return F_bitShiftRight; + else if (strcmp(value, "==") == 0) + return F_equal; + else if (strcmp(value, "!=") == 0) + return F_notEqual; + else if (strcmp(value, ">") == 0) + return F_greaterThen; + else if (strcmp(value, ">=") == 0) + return F_greaterThenOrEqual; + else if (strcmp(value, "<") == 0) + return F_lessThen; + else if (strcmp(value, "<=") == 0) + return F_lessThenOrEqual; + else if (strcmp(value, "&&") == 0) + return F_and; + else if (strcmp(value, "||") == 0) + return F_or; + else if (strcmp(value, "+=") == 0) + return F_plus; + else if (strcmp(value, "-=") == 0) + return F_minus; + else if (strcmp(value, "*=") == 0) + return F_multiplication; + else if (strcmp(value, "/=") == 0) + return F_division; + else if (strcmp(value, "++") == 0) + return F_plusplus; + else if (strcmp(value, "--") == 0) + return F_minmin; + else + return F_NoToken; + +} + +enum Nodes +{ + F_Program, + F_Function, + F_Call, + F_VarDef, + F_Assign, + F_Formal, + F_VarRef, + F_For, + F_If, + F_Cex, + F_Expr, + F_Term, + #ifdef OPTIMIZED_TREE + F_Statement, + F_Indices, + F_Formals, + F_Factor, + F_Block, + F_Actuals, + F_Increment, + F_AssignOperator, + #endif + F_NoNode = 255 +}; + +//Tokens +// ID + +//Optimizer +// level +// index +// external + +// block +// formals +// actuals +// increment +// assignoperator +// type (not used yet in wled) + +//expr +//indices + +const char * nodeToString(uint8_t key) +{ + switch (key) { + case F_Program: + return "program"; + case F_Function: + return "function"; + case F_Call: + return "call"; + case F_VarDef: + return "variable"; + case F_Assign: + return "assign"; + case F_Formal: + return "formal"; + case F_VarRef: + return "varref"; + case F_For: + return "for"; + case F_If: + return "if"; + case F_Cex: + return "cex"; + case F_Expr: + return "expr"; + case F_Term: + return "term"; + #ifdef OPTIMIZED_TREEX + case F_Statement: + return "statement"; + case F_Indices: + return "indices"; + case F_Formals: + return "formals"; + case F_Factor: + return "factor"; + case F_Block: + return "block"; + case F_Actuals: + return "actuals"; + #endif + } + return "unknown key"; +} + +uint8_t stringToNode(const char * node) +{ + if (strcmp(node, "program") == 0) + return F_Program; + else if (strcmp(node, "function") == 0) + return F_Function; + else if (strcmp(node, "call") == 0) + return F_Call; + else if (strcmp(node, "variable") == 0) + return F_VarDef; + else if (strcmp(node, "assign") == 0) + return F_Assign; + else if (strcmp(node, "formal") == 0) + return F_Formal; + else if (strcmp(node, "varref") == 0) + return F_VarRef; + else if (strcmp(node, "for") == 0) + return F_For; + else if (strcmp(node, "if") == 0) + return F_If; + else if (strcmp(node, "cex") == 0) + return F_Cex; + else if (strcmp(node, "expr") == 0) + return F_Expr; + else if (strcmp(node, "term") == 0) + return F_Term; + #ifdef OPTIMIZED_TREE + else if (strcmp(node, "statement") == 0) + return F_Statement; + else if (strcmp(node, "indices") == 0) + return F_Indices; + else if (strcmp(node, "formals") == 0) + return F_Formals; + else if (strcmp(node, "factor") == 0) + return F_Factor; + else if (strcmp(node, "block") == 0) + return F_Block; + else if (strcmp(node, "actuals") == 0) + return F_Actuals; + else if (strcmp(node, "increment") == 0) + return F_Increment; + else if (strcmp(node, "assignoperator") == 0) + return F_AssignOperator; + #endif + return F_NoNode; +} + +bool errorOccurred = false; + +struct Token { + uint16_t lineno; + uint16_t column; + char type[charLength]; + char value[charLength]; +}; + +struct LexerPosition { + uint16_t pos; + char current_char; + uint16_t lineno; + uint16_t column; + char type[charLength]; + char value[charLength]; +}; + +#define nrOfPositions 20 + +class Lexer { + private: + public: + const char * text; + uint16_t pos; + char current_char; + uint16_t lineno; + uint16_t column; + JsonObject definitionJson; + Token current_token; + LexerPosition positions[nrOfPositions]; //should be array of pointers but for some reason get seg fault (because a struct and not a class...) + uint8_t positions_index = 0; + + Lexer(const char * programText, JsonObject definitionJson) { + this->text = programText; + this->definitionJson = definitionJson; + this->pos = 0; + this->current_char = this->text[this->pos]; + this->lineno = 1; + this->column = 1; + } + + ~Lexer() { + DEBUG_ARTI("Destruct Lexer\n"); + } + + void advance() { + if (this->current_char == '\n') + { + this->lineno += 1; + this->column = 0; + } + this->pos++; + + if (this->pos > strlen(this->text) - 1) + this->current_char = -1; + else + { + this->current_char = this->text[this->pos]; + this->column++; + } + } + + void skip_whitespace() + { + while (this->current_char != -1 && isspace(this->current_char)) + this->advance(); + } + + void skip_comment(const char * endTokens) + { + while (strncmp(this->text + this->pos, endTokens, strlen(endTokens)) != 0) + this->advance(); + for (int i=0; iadvance(); + } + + void number() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + char result[charLength] = ""; + while (this->current_char != -1 && isdigit(this->current_char)) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + if (this->current_char == '.') + { + result[strlen(result)] = this->current_char; + this->advance(); + + while (this->current_char != -1 && isdigit(this->current_char)) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + + result[strlen(result)] = '\0'; + strcpy(current_token.type, "REAL_CONST"); + strcpy(current_token.value, result); + } + else + { + result[strlen(result)] = '\0'; + strcpy(current_token.type, "INTEGER_CONST"); + strcpy(current_token.value, result); + } + + } + + void id() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + char result[charLength] = ""; + while (this->current_char != -1 && (isalnum(this->current_char) || this->current_char == '_')) + { + result[strlen(result)] = this->current_char; + this->advance(); + } + result[strlen(result)] = '\0'; + + char resultUpper[charLength]; + strcpy(resultUpper, result); + strupr(resultUpper); + + if (definitionJson["TOKENS"].containsKey(resultUpper)) + { + strcpy(current_token.type, definitionJson["TOKENS"][resultUpper]); + strcpy(current_token.value, resultUpper); + } + else + { + strcpy(current_token.type, "ID"); + strcpy(current_token.value, result); + } + } + + void get_next_token() + { + current_token.lineno = this->lineno; + current_token.column = this->column; + strcpy(current_token.type, ""); + strcpy(current_token.value, ""); + + if (errorOccurred) return; + + while (this->current_char != -1 && this->pos <= strlen(this->text) - 1 && !errorOccurred) + { + if (isspace(this->current_char)) { + this->skip_whitespace(); + continue; + } + + if (strncmp(this->text + this->pos, "/*", 2) == 0) + { + this->advance(); + skip_comment("*/"); + continue; + } + + if (strncmp(this->text + this->pos, "//", 2) == 0) + { + this->advance(); + skip_comment("\n"); + continue; + } + + if (isalpha(this->current_char)) + { + this->id(); + return; + } + + if (isdigit(this->current_char) || (this->current_char == '.' && isdigit(this->text[this->pos+1]))) + { + this->number(); + return; + } + + // findLongestMatchingToken + char token_type[charLength] = ""; + char token_value[charLength] = ""; + + uint8_t longestTokenLength = 0; + + for (JsonPair tokenPair: definitionJson["TOKENS"].as()) { + const char * value = tokenPair.value(); + char currentValue[charLength]; + strncpy(currentValue, this->text + this->pos, charLength); + currentValue[strlen(value)] = '\0'; + if (strcmp(value, currentValue) == 0 && strlen(value) > longestTokenLength) { + strcpy(token_type, tokenPair.key().c_str()); + strcpy(token_value, value); + longestTokenLength = strlen(value); + } + } + + if (strcmp(token_type, "") != 0 && strcmp(token_value, "") != 0) + { + strcpy(current_token.type, token_type); + strcpy(current_token.value, token_value); + for (int i=0; iadvance(); + return; + } + else { + ERROR_ARTI("Lexer error on %c line %u col %u\n", this->current_char, this->lineno, this->column); + errorOccurred = true; + } + } + } //get_next_token + + void eat(const char * token_type) { + // DEBUG_ARTI("try to eat %s %s\n", lexer->current_token.type, token_type); + if (strcmp(current_token.type, token_type) == 0) { + get_next_token(); + // DEBUG_ARTI("eating %s -> %s %s\n", token_type, lexer->current_token.type, lexer->current_token.value); + } + else { + ERROR_ARTI("Lexer Error: Unexpected token %s %s\n", current_token.type, current_token.value); + errorOccurred = true; + } + } + + void push_position() { + if (positions_index < nrOfPositions) { + positions[positions_index].pos = this->pos; + positions[positions_index].current_char = this->current_char; + positions[positions_index].lineno = this->lineno; + positions[positions_index].column = this->column; + strcpy(positions[positions_index].type, current_token.type); + strcpy(positions[positions_index].value, current_token.value); + positions_index++; + } + else + ERROR_ARTI("not enough positions %u\n", nrOfPositions); + } + + void pop_position() { + if (positions_index > 0) { + positions_index--; + this->pos = positions[positions_index].pos; + this->current_char = positions[positions_index].current_char; + this->lineno = positions[positions_index].lineno; + this->column = positions[positions_index].column; + strcpy(current_token.type, positions[positions_index].type); + strcpy(current_token.value, positions[positions_index].value); + } + else + ERROR_ARTI("no positions saved\n"); + } + +}; //Lexer + +#define ResultFail 0 +#define ResultStop 2 +#define ResultContinue 1 + +class ScopedSymbolTable; //forward declaration + +class Symbol { + private: + public: + + uint8_t symbol_type; + char name[charLength]; + uint8_t type; + uint8_t scope_level; + uint8_t scope_index; + ScopedSymbolTable* scope = nullptr; + ScopedSymbolTable* function_scope = nullptr; //used to find the formal parameters in the scope of a function node + + JsonVariant block; + + Symbol(uint8_t symbol_type, const char * name, uint8_t type = 9) { + this->symbol_type = symbol_type; + strcpy(this->name, name); + this->type = type; + this->scope_level = 0; + } + + ~Symbol() { + MEMORY_ARTI("Destruct Symbol %s (%u)\n", name, FREE_SIZE); + } + +}; //Symbol + +#define nrOfSymbolsPerScope 30 +#define nrOfChildScope 10 //add checks + +class ScopedSymbolTable { + private: + public: + + Symbol* symbols[nrOfSymbolsPerScope]; + uint8_t symbolsIndex = 0; + uint8_t nrOfFormals = 0; + char scope_name[charLength]; + uint8_t scope_level; + ScopedSymbolTable *enclosing_scope; + ScopedSymbolTable *child_scopes[nrOfChildScope]; + uint8_t child_scopesIndex = 0; + + ScopedSymbolTable(const char * scope_name, int scope_level, ScopedSymbolTable *enclosing_scope = nullptr) { + strcpy(this->scope_name, scope_name); + this->scope_level = scope_level; + this->enclosing_scope = enclosing_scope; + } + + ~ScopedSymbolTable() { + for (uint8_t i=0; iinsert(BuiltinTypeSymbol('INTEGER')); + // this->insert(BuiltinTypeSymbol('REAL')); + } + + void insert(Symbol* symbol) + { + #ifdef _SHOULD_LOG_SCOPE + ANDBG_ARTI("Log scope Insert %s\n", symbol->name.c_str()); + #endif + symbol->scope_level = this->scope_level; + symbol->scope = this; + symbol->scope_index = symbolsIndex; + if (symbolsIndex < nrOfSymbolsPerScope) + this->symbols[symbolsIndex++] = symbol; + else + ERROR_ARTI("ScopedSymbolTable %s symbols full (%d)", scope_name, nrOfSymbolsPerScope); + } + + Symbol* lookup(const char * name, bool current_scope_only=false) + { + for (uint8_t i=0; iname, name) == 0) + return symbols[i]; + } + + if (current_scope_only) + return nullptr; + // # recursively go up the chain and lookup the name; + if (this->enclosing_scope != nullptr) + return this->enclosing_scope->lookup(name); + + return nullptr; + } //lookup + +}; //ScopedSymbolTable + +#define nrOfVariables 20 + +class ActivationRecord +{ + private: + public: + char name[charLength]; + char type[charLength]; + int nesting_level; + // char charMembers[charLength][nrOfVariables]; + float floatMembers[nrOfVariables]; + char lastSet[charLength]; + uint8_t lastSetIndex; + + ActivationRecord(const char * name, const char * type, int nesting_level) + { + strcpy(this->name, name); + strcpy(this->type, type); + this->nesting_level = nesting_level; + } + + ~ActivationRecord() + { + RUNLOG_ARTI("Destruct activation record %s\n", name); + } + + // void set(uint8_t index, const char * value) + // { + // lastSetIndex = index; + // strcpy(charMembers[index], value); + // } + + void set(uint8_t index, float value) + { + lastSetIndex = index; + floatMembers[index] = value; + } + + // const char * getChar(uint8_t index) + // { + // return charMembers[index]; + // } + + float getFloat(uint8_t index) + { + return floatMembers[index]; + } + +}; //ActivationRecord + +#define nrOfRecords 20 + +class CallStack { +public: + ActivationRecord* records[nrOfRecords]; + uint8_t recordsCounter = 0; + + CallStack() { + } + + ~CallStack() + { + RUNLOG_ARTI("Destruct callstack\n"); + } + + void push(ActivationRecord* ar) + { + if (recordsCounter < nrOfRecords) + { + // RUNLOG_ARTI("%s\n", "Push ", ar->name); + this->records[recordsCounter++] = ar; + } + else + { + errorOccurred = true; + ERROR_ARTI("no space left in callstack\n"); + } + } + + ActivationRecord* pop() + { + if (recordsCounter > 0) + { + // RUNLOG_ARTI("%s\n", "Pop ", this->peek()->name); + return this->records[recordsCounter--]; + } + else + { + ERROR_ARTI("no ar left on callstack\n"); + return nullptr; + } + } + + ActivationRecord* peek() + { + return this->records[recordsCounter-1]; + } +}; //CallStack + +class ValueStack +{ +private: +public: + // char charStack[arrayLength][charLength]; //currently only floatStack used. + float floatStack[arrayLength]; + uint8_t stack_index = 0; + + ValueStack() + { + } + + ~ValueStack() + { + RUNLOG_ARTI("Destruct valueStack\n"); + } + + // void push(const char * value) { + // if (stack_index >= arrayLength) + // ERROR_ARTI("Push charStack full %u of %u\n", stack_index, arrayLength); + // else if (value == nullptr) { + // strcpy(charStack[stack_index++], "empty"); + // ERROR_ARTI("Push null pointer on float stack\n"); + // } + // else + // // RUNLOG_ARTI("calc push %s %s\n", key, value); + // strcpy(charStack[stack_index++], value); + // } + + void push(float value) + { + if (stack_index >= arrayLength) + { + ERROR_ARTI("Push floatStack full (check functions with result assigned) %u\n", arrayLength); + errorOccurred = true; + } + else if (value == floatNull) + ERROR_ARTI("Push null value on float stack\n"); + else + // RUNLOG_ARTI("calc push %s %s\n", key, value); + floatStack[stack_index++] = value; + } + + // const char * peekChar() { + // // RUNLOG_ARTI("Calc Peek %s\n", charStack[stack_index-1]); + // return charStack[stack_index-1]; + // } + + float peekFloat() + { + // RUNLOG_ARTI("Calc Peek %s\n", floatStack[stack_index-1]); + return floatStack[stack_index-1]; + } + + // const char * popChar() { + // if (stack_index>0) { + // stack_index--; + // return charStack[stack_index]; + // } + // else { + // ERROR_ARTI("Pop value stack empty\n"); + // errorOccurred = true; +// // RUNLOG_ARTI("Calc Pop %s\n", charStack[stack_index]); + // return "novalue"; + // } + // } + + float popFloat() + { + if (stack_index>0) + { + stack_index--; + return floatStack[stack_index]; + } + else + { + ERROR_ARTI("Pop floatStack empty\n"); + // RUNLOG_ARTI("Calc Pop %s\n", floatStack[stack_index]); + errorOccurred = true; + return -1; + } + } + +}; //ValueStack + +#define programTextSize 5000 + +class ARTI { +private: + Lexer *lexer = nullptr; + + DynamicJsonDocument *definitionJsonDoc = nullptr; + DynamicJsonDocument *parseTreeJsonDoc = nullptr; + JsonObject definitionJson; + JsonVariant parseTreeJson; + + ScopedSymbolTable *global_scope = nullptr; + CallStack *callStack = nullptr; + ValueStack *valueStack = nullptr; + + uint8_t stages = 5; //for debugging: 0:parseFile, 1:Lexer, 2:parse, 3:optimize, 4:analyze, 5:interpret should be 5 if no debugging + + char logFileName[fileNameLength]; + + uint32_t startMillis; + +public: + ARTI() + { + // MEMORY_ARTI("new Arti < %u\n", FREE_SIZE); //logfile not open here + } + + ~ARTI() + { + MEMORY_ARTI("Destruct ARTI\n"); + } + + //defined in arti_definition.h e.g. arti_wled.h! + float arti_external_function(uint8_t function, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull, float par4 = floatNull, float par5 = floatNull); + float arti_get_external_variable(uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + void arti_set_external_variable(float value, uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + bool loop(); + + uint8_t parse(JsonVariant parseTree, const char * node_name, char operatorx, JsonVariant expression, uint8_t depth = 0) + { + if (depth > 50) + { + ERROR_ARTI("Error: Parse recursion level too deep at %s (%u)\n", parseTree.as().c_str(), depth); + errorOccurred = true; + } + if (errorOccurred) return ResultFail; + + uint8_t result = ResultContinue; + + uint8_t resultChild = ResultContinue; + + if (expression.is()) //should always be the case + { + for (JsonVariant expressionElement: expression.as()) //e.g. ["PROGRAM","ID","block"] + { + const char * nextNode_name = node_name; //e.g. "program": + JsonVariant nextExpression = expressionElement; // e.g. block + JsonVariant nextParseTree = parseTree; + + JsonVariant nodeExpression = lexer->definitionJson[expressionElement.as()]; + + if (!nodeExpression.isNull()) //is expressionElement a Node e.g. "block" : ["LCURL",{"*": ["statement"]},"RCURL"] + { + nextNode_name = expressionElement; //e.g. block + nextExpression = nodeExpression; // e.g. ["LCURL",{"*": ["statement"]},"RCURL"] + + // DEBUG_ARTI("%s %s %u\n", spaces+50-depth, nextNode_name, depth); //, parseTree.as().c_str() + + if (parseTree.is()) + { + parseTree[parseTree.size()][nextNode_name]["connect"] = "array"; + nextParseTree = parseTree[parseTree.size()-1]; //nextparsetree is last element in the array (which is always an object) + } + else //no list, create object + { + if (parseTree[node_name].isNull()) //no object yet + parseTree[node_name]["connect"] = "object"; //make the connection, new object item + + nextParseTree = parseTree[node_name]; + } + } + + if (operatorx == '|') + lexer->push_position(); + + if (nextExpression.is()) // e.g. {"?":["LPAREN","formals*","RPAREN"]} + { + JsonObject::iterator objectIterator = nextExpression.as().begin(); + char objectOperator = objectIterator->key().c_str()[0]; + JsonVariant objectElement = objectIterator->value(); + + if (objectElement.is()) + { + if (objectOperator == '*' || objectOperator == '+') + { + nextParseTree[nextNode_name]["*"][0] = "multiple"; // * is another object in the list of objects + nextParseTree = nextParseTree[nextNode_name]["*"]; + } + + //and: see 'is array' + if (objectOperator == '|') + { + resultChild = parse(nextParseTree, nextNode_name, objectOperator, objectElement, depth + 1); + if (resultChild != ResultFail) resultChild = ResultContinue; + } + else + { + uint8_t resultChild2 = ResultContinue; + uint8_t counter = 0; + while (resultChild2 == ResultContinue) + { + resultChild2 = parse(nextParseTree, nextNode_name, objectOperator, objectElement, depth + 1); //no assign to result as optional + + if (objectOperator == '?') { //zero or one iteration, also continue if parse not continue + resultChild2 = ResultContinue; + break; + } + else if (objectOperator == '+') { //one or more iterations, stop if first parse not continue + if (counter == 0) { + if (resultChild2 != ResultContinue) + break; + } + else + { + if (resultChild2 != ResultContinue) + { + resultChild2 = ResultContinue; //always continue + break; + } + } + } + else if (objectOperator == '*') //zero or more iterations, stop if parse not continue + { + if (resultChild2 != ResultContinue) + { + resultChild2 = ResultContinue; //always continue + break; + } + } + else + { + ERROR_ARTI("%s Programming error: undefined %c %s\n", spaces+50-depth, objectOperator, objectElement.as().c_str()); + resultChild2 = ResultFail; + } + counter++; + } //while + resultChild = resultChild2; + } //not or + } // element is array + else + ERROR_ARTI("%s Definition error: should be an array %s %c %s\n", spaces+50-depth, stringOrEmpty(nextNode_name), operatorx, objectElement.as().c_str()); + } + else if (nextExpression.is()) // e.g. ["LPAREN", "expr*", "RPAREN"] + { + resultChild = parse(nextParseTree, nextNode_name, '&', nextExpression, depth + 1); // every array element starts with '&' (operatorx is for result of all elements of array) + } + else if (lexer->definitionJson["TOKENS"].containsKey(nextExpression.as())) // token e.g. "ID" + { + const char * token_type = nextExpression; + if (strcmp(lexer->current_token.type, token_type) == 0) + { + DEBUG_ARTI("%s %s %s", spaces+50-depth, lexer->current_token.type, lexer->current_token.value); + + //only add 'semantic tokens' + if (strcmp(lexer->current_token.type, "ID") != 0 && strcmp(lexer->current_token.type, "INTEGER_CONST") != 0 && strcmp(lexer->current_token.type, "REAL_CONST") != 0 && + strcmp(lexer->current_token.type, "INTEGER") != 0 && strcmp(lexer->current_token.type, "REAL") != 0 && + strcmp(lexer->current_token.value, "+") != 0 && strcmp(lexer->current_token.value, "-") != 0 && strcmp(lexer->current_token.value, "*") != 0 && strcmp(lexer->current_token.value, "/") != 0 && strcmp(lexer->current_token.value, "%") != 0 && + strcmp(lexer->current_token.value, "+=") != 0 && strcmp(lexer->current_token.value, "-=") != 0 && strcmp(lexer->current_token.value, "*=") != 0 && strcmp(lexer->current_token.value, "/=") != 0 && + strcmp(lexer->current_token.value, "<<") != 0 && strcmp(lexer->current_token.value, ">>") != 0 && + strcmp(lexer->current_token.value, "==") != 0 && strcmp(lexer->current_token.value, "!=") != 0 && + strcmp(lexer->current_token.value, "&&") != 0 && strcmp(lexer->current_token.value, "||") != 0 && + strcmp(lexer->current_token.value, ">") != 0 && strcmp(lexer->current_token.value, ">=") != 0 && strcmp(lexer->current_token.value, "<") != 0 && strcmp(lexer->current_token.value, "<=") != 0 && + strcmp(lexer->current_token.value, "++") != 0 && strcmp(lexer->current_token.value, "--") != 0 + ) + {} + else + { + if (nextParseTree.is()) + nextParseTree.as()[nextParseTree.size()][lexer->current_token.type] = lexer->current_token.value; //add in last element of array + else + nextParseTree[nextNode_name][lexer->current_token.type] = lexer->current_token.value; + } + + lexer->eat(token_type); + + DEBUG_ARTI(" -> [%s %s] %d\n", lexer->current_token.type, lexer->current_token.value, depth); + + resultChild = ResultContinue; + } + else //deadend + resultChild = ResultFail; + } // if token + else //expressionElement is not a node, not a token, not an array and not an object + { + if (lexer->definitionJson.containsKey(nextExpression.as())) + ERROR_ARTI("%s Programming error: %s not a node, token, array or object in %s\n", spaces+50-depth, nextExpression.as().c_str(), stringOrEmpty(nextNode_name)); + else + ERROR_ARTI("%s Definition error: \"%s\": \"%s\" node should be embedded in array\n", spaces+50-depth, stringOrEmpty(nextNode_name), nextExpression.as().c_str()); + } //nextExpression is not a token + + if (!nodeExpression.isNull()) //if node + { + if (nextParseTree.containsKey("connect")) + nextParseTree.remove("connect"); //remove connector + // for values which are not parsed deeper. e.g. : {"formal": {"ID": "z"}} + for (JsonPair parseTreePair : nextParseTree.as()) + { + JsonVariant value = parseTreePair.value(); + if (value.containsKey("connect")) + value.remove("connect"); //remove connector + } + + if (resultChild == ResultFail) { //remove result of parse + nextParseTree.remove(nextNode_name); //remove the failed stuff + + // DEBUG_ARTI("%s fail %s\n", spaces+50-depth, nextNode_name); + } + else //success + { + DEBUG_ARTI("%s found %s\n", spaces+50-depth, nextNode_name);//, nextParseTree.as().c_str()); + //parseTree optimization moved to optimize function + + // if (nextParseTree.is()) + // { + + // // optimize(nextParseTree, depth); + + // JsonObject innerObject = nextParseTree[nextNode_name].as(); + + // JsonObject::iterator begin = innerObject.begin(); + // if (fnextParseTree.size() == 1 && nextParseTree[nextNode_name].size() == 1 && lexer->definitionJson.containsKey(nextNode_name) && lexer->definitionJson.containsKey(nextNode_name) && lexer->definitionJson.containsKey(begin->key())) + // { + // // JsonObject nextParseTreeObject = nextParseTree.as(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, parseTree.as().c_str()); + // DEBUG_ARTI("%s found replace %s by %s %s\n", spaces+50-depth, nextNode_name, begin->key().c_str(), nextParseTree.as().c_str()); + // DEBUG_ARTI("%s expression %s\n", spaces+50-depth, expression.as().c_str()); + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, nextParseTree[nextNode_name].as().c_str()); + + // nextParseTree.remove(nextNode_name); + // // char temp[charLength]; + // // strcpy(temp, nextNode_name); + // // strcat(temp, "-"); + // // strcat(temp, begin->key().c_str()); + // nextParseTree[begin->key()] = begin->value(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, parseTree.as().c_str()); + // } + // } + // else + // DEBUG_ARTI("%s no jsonobject??? %s\n", spaces+50-depth, parseTree.as().c_str()); + + } + } // if node + + //determine result of expressionElement + if (operatorx == '|') { + if (resultChild == ResultFail) {//if fail, go back and try another + // result = ResultContinue; + lexer->pop_position(); + } + else { + result = ResultStop; //Stop or continue is enough for an or + lexer->positions_index--; + } + } + else { + if (resultChild != ResultContinue) //for and, ?, + and *; each result should continue + result = ResultFail; + } + + if (result != ResultContinue) //if no reason to continue then stop + break; + + } //for expressionElement + + if (operatorx == '|') + { + if (result != ResultStop) //still looking but nothing to look for + result = ResultFail; + } + } + else { //should never happen + ERROR_ARTI("%s Programming error: no array %s %c %s\n", spaces+50-depth, stringOrEmpty(node_name), operatorx, expression.as().c_str()); + } + + return result; + + } //parse + + bool analyze(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + { + // ANDBG_ARTI("%s Depth %u %s\n", spaces+50-depth, depth, parseTree.as().c_str()); + if (depth > 24) //otherwise stack canary errors on Arduino (value determined after testing, should be revisited) + { + ERROR_ARTI("Error: Analyze recursion level too deep at %s (%u)\n", parseTree.as().c_str(), depth); + errorOccurred = true; + } + if (errorOccurred) return false; + + if (parseTree.is()) + { + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + if (treeElement == nullptr || strcmp(treeElement, key) == 0 ) //in case there are more elements in the object and you want to analyze only one + { + bool visitedAlready = false; + + if (strcmp(key, "*") == 0 ) // multiple + { + } + else if (strcmp(key, "token") == 0) // do nothing with added tokens + visitedAlready = true; + else if (this->definitionJson["TOKENS"].containsKey(key)) // if token + { + const char * valueStr = value; + + if (strcmp(key, "INTEGER_CONST") == 0) + parseTree["token"] = F_integerConstant; + else if (strcmp(key, "REAL_CONST") == 0) + parseTree["token"] = F_realConstant; + else if (strcmp(valueStr, "+") == 0) + parseTree["token"] = F_plus; + else if (strcmp(valueStr, "-") == 0) + parseTree["token"] = F_minus; + else if (strcmp(valueStr, "*") == 0) + parseTree["token"] = F_multiplication; + else if (strcmp(valueStr, "/") == 0) + parseTree["token"] = F_division; + else if (strcmp(valueStr, "%") == 0) + parseTree["token"] = F_modulo; + else if (strcmp(valueStr, "<<") == 0) + parseTree["token"] = F_bitShiftLeft; + else if (strcmp(valueStr, ">>") == 0) + parseTree["token"] = F_bitShiftRight; + else if (strcmp(valueStr, "==") == 0) + parseTree["token"] = F_equal; + else if (strcmp(valueStr, "!=") == 0) + parseTree["token"] = F_notEqual; + else if (strcmp(valueStr, ">") == 0) + parseTree["token"] = F_greaterThen; + else if (strcmp(valueStr, ">=") == 0) + parseTree["token"] = F_greaterThenOrEqual; + else if (strcmp(valueStr, "<") == 0) + parseTree["token"] = F_lessThen; + else if (strcmp(valueStr, "<=") == 0) + parseTree["token"] = F_lessThenOrEqual; + else if (strcmp(valueStr, "&&") == 0) + parseTree["token"] = F_and; + else if (strcmp(valueStr, "||") == 0) + parseTree["token"] = F_or; + else + ERROR_ARTI("%s Programming error: token not defined as operator %s %s (%u)\n", spaces+50-depth, key, value.as().c_str(), depth); + + visitedAlready = true; + } + else //if key is node_name + { + uint8_t node = stringToNode(key); + + switch (node) + { + case F_Program: + { + const char * program_name = value["ID"]; + global_scope = new ScopedSymbolTable(program_name, 1, nullptr); //current_scope + + ANDBG_ARTI("%s Program %s %u %u\n", spaces+50-depth, global_scope->scope_name, global_scope->scope_level, global_scope->symbolsIndex); + + if (value["ID"].isNull()) { + ERROR_ARTI("program name null\n"); + errorOccurred = true; + } + if (value["block"].isNull()) { + ERROR_ARTI("%s Program %s: no block in parseTree\n", spaces+50-depth, program_name); + errorOccurred = true; + } + else { + analyze(value["block"], nullptr, global_scope, depth + 1); + } + + #ifdef ARTI_DEBUG + for (uint8_t i=0; isymbolsIndex; i++) { + Symbol* symbol = global_scope->symbols[i]; + ANDBG_ARTI("%s %u %s %s.%s of %u (%u)\n", spaces+50-depth, i, nodeToString(symbol->symbol_type), global_scope->scope_name, symbol->name, symbol->type, symbol->scope_level); + } + #endif + + visitedAlready = true; + break; + } + case F_Function: + { + //find the function name (so we must know this is a function...) + const char * function_name = value["ID"]; + Symbol* function_symbol = new Symbol(node, function_name); + current_scope->insert(function_symbol); + + ANDBG_ARTI("%s Function %s.%s\n", spaces+50-depth, current_scope->scope_name, function_name); + ScopedSymbolTable* function_scope = new ScopedSymbolTable(function_name, current_scope->scope_level + 1, current_scope); + if (current_scope->child_scopesIndex < nrOfChildScope) + current_scope->child_scopes[current_scope->child_scopesIndex++] = function_scope; + else + ERROR_ARTI("ScopedSymbolTable %s childs full (%d)", current_scope->scope_name, nrOfChildScope); + function_symbol->function_scope = function_scope; + + if (value.containsKey("formals")) + analyze(value["formals"], nullptr, function_scope, depth + 1); + + function_scope->nrOfFormals = function_scope->symbolsIndex; + + if (value["block"].isNull()) + ERROR_ARTI("%s Function %s: no block in parseTree\n", spaces+50-depth, function_name); + else + analyze(value["block"], nullptr, function_scope, depth + 1); + + #ifdef ARTI_DEBUG + for (uint8_t i=0; isymbolsIndex; i++) { + Symbol* symbol = function_scope->symbols[i]; + ANDBG_ARTI("%s %u %s %s.%s of %u (%u)\n", spaces+50-depth, i, nodeToString(symbol->symbol_type), function_scope->scope_name, symbol->name, symbol->type, symbol->scope_level); + } + #endif + + visitedAlready = true; + break; + } + case F_VarDef: + case F_Formal: + case F_Assign: + case F_VarRef: + { + JsonObject variable_value; + if (node == F_Assign) + variable_value = value["varref"]; + else + variable_value = value; + + const char * variable_name; + variable_name = variable_value["ID"]; + + if (variable_value.containsKey("indices")) + analyze(variable_value, "indices", current_scope, depth + 1); + + //check if external variable + bool externalFound = false; + uint8_t index = 0; + for (JsonPair externalsPair: definitionJson["EXTERNALS"].as()) { + if (strcmp(variable_name, externalsPair.key().c_str()) == 0) { + variable_value["external"] = index; //add external index to parseTree + ANDBG_ARTI("%s Ext Variable found %s (%u) %s\n", spaces+50-depth, variable_name, depth, key); + externalFound = true; + } + index++; + } + + if (!externalFound) + { + Symbol* var_symbol = current_scope->lookup(variable_name); //lookup here and parent scopes + if (node == F_VarRef) + { + if (var_symbol == nullptr) + WARNING_ARTI("%s VarRef %s ID not found in scope of %s\n", spaces+50-depth, variable_name, current_scope->scope_name); + //only warning: value 0 in interpreter (div 0 is captured) + else + ANDBG_ARTI("%s VarRef found %s.%s (%u)\n", spaces+50-depth, var_symbol->scope->scope_name, variable_name, depth); + } + else //assign and var/formal + { + //if variable not already defined, then add + if (node != F_Assign || var_symbol == nullptr) //only assign needs to check if not exists + { + char param_type[charLength]; + if (variable_value.containsKey("type")) + serializeJson(variable_value["type"], param_type); //current_scope.lookup(param.type_node.value); //need string, lookup also used to find types... + else + strcpy(param_type, "notype"); + + var_symbol = new Symbol(node, variable_name, 9); // no type support yet + if (node == F_Assign) + global_scope->insert(var_symbol); // assigned variables are global scope + else + current_scope->insert(var_symbol); + + ANDBG_ARTI("%s %s %s.%s of %s\n", spaces+50-depth, key, var_symbol->scope->scope_name, variable_name, param_type); + } + else if (node != F_Assign && var_symbol != nullptr) + ERROR_ARTI("%s %s Duplicate ID %s.%s\n", spaces+50-depth, key, var_symbol->scope->scope_name, variable_name); + + } + + if (var_symbol != nullptr) + { + variable_value["level"] = var_symbol->scope_level; + variable_value["index"] = var_symbol->scope_index;; + } + } + + if (node == F_Assign) + { + ANDBG_ARTI("%s %s %s = (%u)\n", spaces+50-depth, key, variable_name, depth); + + if (value.containsKey("assignoperator")) + { + JsonObject asop = value["assignoperator"]; + JsonObject::iterator asopBegin = asop.begin(); + ANDBG_ARTI("%s %s\n", spaces+50-depth, asopBegin->value().as().c_str()); + if (strcmp(asopBegin->value(), "+=") == 0) + value["assignoperator"] = F_plus; + else if (strcmp(asopBegin->value(), "-=") == 0) + value["assignoperator"] = F_minus; + else if (strcmp(asopBegin->value(), "*=") == 0) + value["assignoperator"] = F_multiplication; + else if (strcmp(asopBegin->value(), "/=") == 0) + value["assignoperator"] = F_division; + else if (strcmp(asopBegin->value(), "++") == 0) + value["assignoperator"] = F_plusplus; + else if (strcmp(asopBegin->value(), "--") == 0) + value["assignoperator"] = F_minmin; + } + + if (value.containsKey("expr")) + analyze(value, "expr", current_scope, depth + 1); + else if (value["assignoperator"].as() != F_plusplus && value["assignoperator"].as() != F_minmin) + { + ERROR_ARTI("%s %s %s: Assign without expression\n", spaces+50-depth, key, variable_name); + errorOccurred = true; + } + } + + visitedAlready = true; + break; + } + case F_Call: + { + const char * function_name = value["ID"]; + + //check if external function + bool externalFound = false; + uint8_t index = 0; + for (JsonPair externalsPair: definitionJson["EXTERNALS"].as()) + { + if (strcmp(function_name, externalsPair.key().c_str()) == 0) + { + ANDBG_ARTI("%s Ext Function found %s (%u)\n", spaces+50-depth, function_name, depth); + value["external"] = index; //add external index to parseTree + externalFound = true; + } + index++; + } + + if (!externalFound) + { + Symbol* function_symbol = current_scope->lookup(function_name); //lookup here and parent scopes + if (function_symbol != nullptr) + analyze(function_symbol->block, nullptr, current_scope, depth + 1); + else + ERROR_ARTI("%s Function %s not found in scope of %s\n", spaces+50-depth, function_name, current_scope->scope_name); + } //external functions + + if (value.containsKey("actuals")) + analyze(value["actuals"], nullptr, current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + default: //visitedalready false => recursive call + break; + } //switch + } // is node_name + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + analyze(value, nullptr, current_scope, depth + 1); + + } // key values + } ///for elements in object + } + else if (parseTree.is()) + { + for (JsonVariant newParseTree: parseTree.as()) + analyze(newParseTree, nullptr, current_scope, depth + 1); + } + else //not array + { + // string element = parseTree; + //for some weird reason this causes a crash on esp32 + // ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, parseTree.as().c_str(), depth); + } + + return !errorOccurred; + } //analyze + + //https://dev.to/lefebvre/compilers-106---optimizer--ig8 + bool optimize(JsonVariant parseTree, uint8_t depth = 0) + { + // DEBUG_ARTI("%s optimize %s (%u)\n", spaces+50-depth, parseTree.as().c_str(), depth); + + if (parseTree.is()) + { + //make the parsetree as small as possible to let the interpreter run as fast as possible: + + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + + bool visitedAlready = false; + + // object: + // if key is node and value is object then optimize object after that if object empty then remove key + // else key is !ID and value is string then remove key + // else key is * and value is array then optimize array after that if array empty then remove key * + + // array + // if element is multiple then remove + + if (strcmp(key, "*") == 0 ) // multiple + { + optimize(value, depth + 1); + + if (value.size() == 0) + { + parseTree.remove(key); + // DEBUG_ARTI("%s optimize: remove empty %s %s (%u)\n", spaces+50-depth, key, value.as().c_str(), depth); + } + + visitedAlready = true; + } + else if (this->definitionJson["TOKENS"].containsKey(key)) // if key is token (moved to parse) + { + visitedAlready = true; + } + else if (value.is()) //if key is node_name + { + optimize(value, depth + 1); + + if (value.size() == 0) + { + // DEBUG_ARTI("%s optimize: remove key %s with empty object (%u)\n", spaces+50-depth, key, depth); + parseTree.remove(key); + } + else if (value.size() == 1) //try to shrink, moved to below + { + //- check if a node is not used in analyzer / interpreter and has only one element: go to the parent and replace itself with its child (shrink) + + // DEBUG_ARTI("%s node try to shrink %s : %s (%u)\n", spaces+50-depth, key, value.as().c_str(), value.size()); + + JsonObject::iterator objectIterator = value.as().begin(); + + if (definitionJson.containsKey(objectIterator->key().c_str())) // if value key is a node + { + // if (objectIterator->value().is() && objectIterator->value().size() == 1) // + // { + // // JsonObject::iterator objectIterator2 = objectIterator->value().as().begin(); + + // // if (objectIterator2->value().is() ) //&& objectIterator.size() == 1 + // { + // DEBUG_ARTI("%s node to shrink %s : %s from %s\n", spaces+50-depth, key, value.as().c_str(), parseTree.as().c_str()); + // DEBUG_ARTI("%s node to shrink %s : %s\n", spaces+50-depth, objectIterator->key().c_str(), objectIterator->value().as().c_str()); + // // DEBUG_ARTI("%s node to shrink %s : %s\n", spaces+50-depth, objectIterator2->key().c_str(), objectIterator2->value().as().c_str()); + // DEBUG_ARTI("%s node to shrink replace %s\n", spaces+50-depth, parseTree[key].as().c_str()); + // DEBUG_ARTI("%s node to shrink by %s\n", spaces+50-depth, objectIterator->value().as().c_str()); + // // parseTree[key][objectIterator->key().c_str()] = objectIterator2->value(); + // parseTree[key] = objectIterator->value(); + // } + // // else + // // { + // // DEBUG_ARTI("%s node to shrink2 %s : %s\n", spaces+50-depth, objectIterator2->key().c_str(), objectIterator2->value().as().c_str()); + // // } + // } + // else + // DEBUG_ARTI("%s value should be an object %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, objectIterator->value().as().c_str(), value.as().c_str()); + if (stringToNode(objectIterator->key().c_str()) == F_NoNode) // if key not a node + // if (objectIterator->value().size() == 1) + { + DEBUG_ARTI("%s node to shrink %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, value.as().c_str(), parseTree.as().c_str()); + // DEBUG_ARTI("%s node to shrink %s in %s = %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, objectIterator->value().as().c_str(), parseTree[key].as().c_str()); + parseTree[key] = objectIterator->value(); + + // parseTree[key]["old"] = objectIterator->key(); + + // parseTreePair.key() = objectIterator->key(); + // parseTreePair._key = objectIterator->key(); + // parseTree[objectIterator->key()] = objectIterator->value(); + // parseTree.remove(key); + // parseTree[key][objectIterator2->key().c_str()] = objectIterator2->value(); + } + } + } //shrink + + visitedAlready = true; + } + else + ERROR_ARTI("%s Programming Error: key no node and no token %s %s (%u)\n", spaces+50-depth, key, value.as().c_str(), depth); + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + optimize(value, depth + 1); + + } ///for elements in object + + //shrink + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + + if (false && value.is() && parseTree.size() == 1 && value.size() == 1 && definitionJson.containsKey(key)) //if key is node_name + { + JsonObject::iterator objectIterator = value.as().begin(); + + // DEBUG_ARTI("%s try replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), parseTree.as().c_str()); + + if (strcmp(objectIterator->key().c_str(), "ID") != 0) //&& definitionJson.containsKey(objectIterator->key())??? + { + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, parseTree.as().c_str()); + // DEBUG_ARTI("%s found replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), parseTree.as().c_str()); + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, value.as().c_str()); + + // DEBUG_ARTI("%s found %s\n", spaces+50-depth, parseTree.as().c_str()); + DEBUG_ARTI("%s replace %s by %s %s\n", spaces+50-depth, key, objectIterator->key().c_str(), parseTree.as().c_str()); + + parseTree.remove(key); + // parseTree[key] = value; + parseTree[objectIterator->key()] = objectIterator->value(); + + // DEBUG_ARTI("%s found %s:%s\n", spaces+50-depth, node_name, parseTree.as().c_str()); + + } + // else + // { + // DEBUG_ARTI("%s not shrinkable %s %s\n", spaces+50-depth, key, value.as().c_str()); + // if (depth > 12) { + // // parseTree.remove(key); + // char temp[charLength]; + // strcpy(temp, key); + // strcat(temp, "-"); + // // strcat(temp, objectIterator->key().c_str()); + // // parseTree[temp] = value; + // } + // } + + if (false && definitionJson.containsKey(objectIterator->key().c_str())) // if value key is a node + { + if (stringToNode(objectIterator->key().c_str()) == F_NoNode) // if key not a node + // if (objectIterator->value().size() == 1) + { + DEBUG_ARTI("%s node to shrink %s in %s : %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, value.as().c_str(), parseTree.as().c_str()); + // DEBUG_ARTI("%s node to shrink %s in %s = %s from %s\n", spaces+50-depth, objectIterator->key().c_str(), key, objectIterator->value().as().c_str(), parseTree[key].as().c_str()); + parseTree[key] = objectIterator->value(); + + // parseTree[key]["old"] = objectIterator->key(); + + // parseTreePair.key() = objectIterator->key(); + // parseTreePair._key = objectIterator->key(); + // parseTree[objectIterator->key()] = objectIterator->value(); + // parseTree.remove(key); + // parseTree[key][objectIterator2->key().c_str()] = objectIterator2->value(); + } + } + } //value is jsonObject + + } // for + } + else if (parseTree.is()) + { + JsonArray parseTreeArray = parseTree.as(); + + uint8_t arrayIndex = 0; + for (JsonArray::iterator it = parseTreeArray.begin(); it != parseTreeArray.end(); ++it) + { + JsonVariant element = *it; + + if (element == "multiple") + { + // DEBUG_ARTI("%s optimize: remove array element 'multiple' of array (%u)\n", spaces+50-depth, arrayIndex); + parseTreeArray.remove(it); + } + else if (it->size() == 0) //remove {} elements (added by * arrays, don't know where added) + { + // DEBUG_ARTI("%s optimize: remove array element {} of %s array (%u)\n", spaces+50-depth, element.as().c_str(), arrayIndex); + parseTreeArray.remove(it); + } + else + optimize(*it, depth + 1); + + arrayIndex++; + } + } + else //not array + { + // string element = parseTree; + //for some weird reason this causes a crash on esp32 + ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, parseTree.as().c_str(), depth); + } + + // DEBUG_ARTI("%s optimized %s (%u)\n", spaces+50-depth, parseTree.as().c_str(), depth); + + return !errorOccurred; + } //optimize + + // bool visit_ID(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + + bool interpret(JsonVariant parseTree, const char * treeElement = nullptr, ScopedSymbolTable* current_scope = nullptr, uint8_t depth = 0) + { + // RUNLOG_ARTI("%s Interpret %s %s (%u)\n", spaces+50-depth, stringOrEmpty(treeElement), parseTree.as().c_str(), depth); + + if (depth >= 50) + { + ERROR_ARTI("Error: Interpret recursion level too deep at %s (%u)\n", parseTree.as().c_str(), depth); + errorOccurred = true; + } + if (errorOccurred) return false; + + if (parseTree.is()) + { + for (JsonPair parseTreePair : parseTree.as()) + { + const char * key = parseTreePair.key().c_str(); + JsonVariant value = parseTreePair.value(); + if (treeElement == nullptr || strcmp(treeElement, key) == 0) + { + // RUNLOG_ARTI("%s Interpret object element %s %s\n", spaces+50-depth, key, value.as().c_str()); + + bool visitedAlready = false; + + if (strcmp(key, "*") == 0) + { + // do the recursive call below + } + else if (strcmp(key, "token") == 0 || strcmp(key, "variable") == 0) //variable decls done in analyze (see pas) + visitedAlready = true; + else if (parseTree.containsKey("token")) //key is token + { + // RUNLOG_ARTI("%s Token %s %s %s\n", spaces+50-depth, key, valueStr, parseTree.as().c_str()); + + const char * valueStr = value; + + switch (parseTree["token"].as()) + { + case F_integerConstant: + case F_realConstant: + valueStack->push(atof(valueStr)); //push value + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s (Push %u)\n", spaces+50-depth, key, valueStr, valueStack->stack_index); + #endif + break; + default: + valueStack->push(parseTree["token"].as()); // push Operator index + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s (Push %u)\n", spaces+50-depth, key, valueStr, valueStack->stack_index); + #endif + } + visitedAlready = true; + } + else //if key is node_name + { + uint8_t node = stringToNode(key); + + // RUNLOG_ARTI("%s Node %s\n", spaces+50-depth, key); + + switch (node) + { + case F_Program: + { + const char * program_name = value["ID"]; + RUNLOG_ARTI("%s program %s\n", spaces+50-depth, program_name); + + ActivationRecord* ar = new ActivationRecord(program_name, "PROGRAM", 1); + + this->callStack->push(ar); + + interpret(value["block"], nullptr, global_scope, depth + 1); + + // do not delete main stack and program ar as used in subsequent calls + // this->callStack->pop(); + // delete ar; ar = nullptr; + + visitedAlready = true; + break; + } + case F_Function: + { + const char * function_name = value["ID"]; + Symbol* function_symbol = current_scope->lookup(function_name); + RUNLOG_ARTI("%s Save block of %s\n", spaces+50-depth, function_name); + if (function_symbol != nullptr) + function_symbol->block = value["block"]; + else + ERROR_ARTI("%s Function %s: not found\n", spaces+50-depth, function_name); + + visitedAlready = true; + break; + } + case F_Call: + { + const char * function_name = value["ID"]; + + //check if external function + if (value.containsKey("external")) { + uint8_t oldIndex = valueStack->stack_index; + + if (value.containsKey("actuals")) + interpret(value["actuals"], nullptr, current_scope, depth + 1); + + float returnValue = floatNull; + + returnValue = arti_external_function(value["external"], valueStack->floatStack[oldIndex] + , (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull + , (valueStack->stack_index - oldIndex>2)?valueStack->floatStack[oldIndex+2]:floatNull + , (valueStack->stack_index - oldIndex>3)?valueStack->floatStack[oldIndex+3]:floatNull + , (valueStack->stack_index - oldIndex>4)?valueStack->floatStack[oldIndex+4]:floatNull); + + #if ARTI_PLATFORM != ARTI_ARDUINO // because arduino runs the code instead of showing the code + uint8_t lastIndex = oldIndex; + RUNLOG_ARTI("%s Call %s(", spaces+50-depth, function_name); + char sep[3] = ""; + for (int i = oldIndex; i< valueStack->stack_index; i++) { + RUNLOG_ARTI("%s%f", sep, valueStack->floatStack[i]); + strcpy(sep, ", "); + } + if ( returnValue != floatNull) + RUNLOG_ARTI(") = %f (Pop %u, Push %u)\n", returnValue, oldIndex, oldIndex + 1); + else + RUNLOG_ARTI(") (Pop %u)\n", oldIndex); + + #endif + + valueStack->stack_index = oldIndex; + + if (returnValue != floatNull) + valueStack->push(returnValue); + + } + else { //not an external function + Symbol* function_symbol = current_scope->lookup(function_name); + + if (function_symbol != nullptr) //calling undefined function: pre-defined functions e.g. print + { + ActivationRecord* ar = new ActivationRecord(function_name, "Function", function_symbol->scope_level + 1); + + RUNLOG_ARTI("%s %s %s\n", spaces+50-depth, key, function_name); + + uint8_t oldIndex = valueStack->stack_index; + uint8_t lastIndex = valueStack->stack_index; + + if (value.containsKey("actuals")) + interpret(value["actuals"], nullptr, current_scope, depth + 1); + + for (uint8_t i=0; ifunction_scope->nrOfFormals; i++) + { + //determine type, for now assume float + float result = valueStack->floatStack[lastIndex++]; + ar->set(function_symbol->function_scope->symbols[i]->scope_index, result); + RUNLOG_ARTI("%s Actual %s.%s = %f (pop %u)\n", spaces+50-depth, function_name, function_symbol->function_scope->symbols[i]->name, result, valueStack->stack_index); + } + + valueStack->stack_index = oldIndex; + + this->callStack->push(ar); + + interpret(function_symbol->block, nullptr, function_symbol->function_scope, depth + 1); + + this->callStack->pop(); + + delete ar; ar = nullptr; + + //tbd if syntax supports returnvalue + // char callResult[charLength] = "CallResult tbd of "; + // strcat(callResult, function_name); + // valueStack->push(callResult); + + } //function_symbol != nullptr + else + RUNLOG_ARTI("%s %s not found %s\n", spaces+50-depth, key, function_name); + } //external functions + + visitedAlready = true; + break; + } + case F_VarRef: + case F_Assign: //get or set a variable + { + const char * variable_name; + uint8_t variable_level; + uint8_t variable_index; + uint8_t variable_external; + JsonObject variable_indices; + JsonObject variable_value; + + float resultValue = floatNull; + + if (node == F_Assign) + { + variable_value = value["varref"]; + + if (value.containsKey("expr")) //value assignment + { + interpret(value, "expr", current_scope, depth + 1); //value pushed + + resultValue = valueStack->popFloat(); //result of interpret expr (but not for -- and ++ !!!!) + } + } + else + variable_value = value; + + variable_name = variable_value["ID"]; + variable_level = variable_value["level"]; + variable_index = variable_value["index"]; + variable_external = variable_value["external"]; + variable_indices = variable_value["indices"]; + + uint8_t oldIndex = valueStack->stack_index; + + //array indices + char indices[charLength]; //used in RUNLOG_ARTI only + strcpy(indices, ""); + if (!variable_indices.isNull()) + { + strcat(indices, "["); + + interpret(variable_value, "indices", current_scope, depth + 1); //values of indices pushed + + char sep[3] = ""; + for (uint8_t i = oldIndex; i< valueStack->stack_index; i++) { + strcat(indices, sep); + char itoaChar[charLength]; + // itoa(valueStack->floatStack[i], itoaChar, 10); + snprintf(itoaChar, sizeof(itoaChar), "%f", valueStack->floatStack[i]); + strcat(indices, itoaChar); + strcpy(sep, ","); + } + + strcat(indices, "]"); + } + + //check if external variable + if (variable_value.containsKey("external")) //added by Analyze + { + + if (node == F_VarRef) { //get the value + + resultValue = arti_get_external_variable(variable_external, (valueStack->stack_index - oldIndex>0)?valueStack->floatStack[oldIndex]:floatNull, (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull); + valueStack->stack_index = oldIndex; + + if (resultValue != floatNull) + { + valueStack->push(resultValue); + RUNLOG_ARTI("%s %s ext.%s = %f (push %u)\n", spaces+50-depth, key, variable_name, resultValue, valueStack->stack_index); //key is variable_declaration name is ID + } + else + ERROR_ARTI("%s Error: %s ext.%s no value\n", spaces+50-depth, key, variable_name); + } + else //assign: set the external value... + { + arti_set_external_variable(resultValue, variable_external, (valueStack->stack_index - oldIndex>0)?valueStack->floatStack[oldIndex]:floatNull, (valueStack->stack_index - oldIndex>1)?valueStack->floatStack[oldIndex+1]:floatNull); + valueStack->stack_index = oldIndex; + + RUNLOG_ARTI("%s %s set ext.%s%s = %f (Pop %u)\n", spaces+50-depth, key, variable_name, indices, resultValue, oldIndex); + } + } + else //not external, get er set the variable + { + // Symbol* variable_symbol = current_scope->lookup(variable_name); + ActivationRecord* ar; + + //check already defined in this scope + + // RUNLOG_ARTI("%s levels %u-%u\n", spaces+50-depth, variable_level, variable_index ); + if (variable_level != 0) { //var already exist + //calculate the index in the call stack to find the right ar + uint8_t index = this->callStack->recordsCounter - 1 - (this->callStack->peek()->nesting_level - variable_level); + // RUNLOG_ARTI("%s %s %s.%s = %s (push) %s %d-%d = %d (%d)\n", spaces+50-depth, key, ar->name, variable_name, varValue, variable_symbol->name, this->callStack->peek()->nesting_level,variable_symbol->scope_level, index, this->callStack->recordsCounter); //key is variable_declaration name is ID + ar = this->callStack->records[index]; + } + else //var created here + ar = this->callStack->peek(); + + if (ar != nullptr) // variable found + { + if (node == F_VarRef) //get the value + { + //determine type, for now assume float + float varValue = ar->getFloat(variable_index); + + valueStack->push(varValue); + #if ARTI_PLATFORM != ARTI_ARDUINO //for some weird reason this causes a crash on esp32 + RUNLOG_ARTI("%s %s %s.%s = %f (push %u) %u-%u\n", spaces+50-depth, key, ar->name, variable_name, varValue, valueStack->stack_index, variable_level, variable_index); //key is variable_declaration name is ID + #endif + } + else { //assign: set the value + + if (value.containsKey("assignoperator")) + { + switch (value["assignoperator"].as()) + { + case F_plus: + ar->set(variable_index, ar->getFloat(variable_index) + resultValue); + break; + case F_minus: + ar->set(variable_index, ar->getFloat(variable_index) - resultValue); + break; + case F_multiplication: + ar->set(variable_index, ar->getFloat(variable_index) * resultValue); + break; + case F_division: + { + if (resultValue == 0) // divisor + { + resultValue = 1; + ERROR_ARTI("%s /= division by 0 not possible, divisor ignored for %f\n", spaces+50-depth, ar->getFloat(variable_index)); + } + ar->set(variable_index, ar->getFloat(variable_index) / resultValue); + break; + } + case F_plusplus: + ar->set(variable_index, ar->getFloat(variable_index) + 1); + break; + case F_minmin: + ar->set(variable_index, ar->getFloat(variable_index) - 1); + break; + } + + RUNLOG_ARTI("%s %s.%s%s %s= %f (pop %u) %u-%u\n", spaces+50-depth, ar->name, variable_name, indices, tokenToString(value["assignoperator"]), ar->getFloat(variable_index), valueStack->stack_index, variable_level, variable_index); + } + else + { + ar->set(variable_index, resultValue); + RUNLOG_ARTI("%s %s.%s%s := %f (pop %u) %u-%u\n", spaces+50-depth, ar->name, variable_name, indices, ar->getFloat(variable_index), valueStack->stack_index, variable_level, variable_index); + } + valueStack->stack_index = oldIndex; + } + } //ar != nullptr + else { //unknown variable + ERROR_ARTI("%s %s %s unknown\n", spaces+50-depth, key, variable_name); + valueStack->push(floatNull); + } + } // ! founnd + visitedAlready = true; + break; + } + case F_Expr: + case F_Term: + { + uint8_t oldIndex = valueStack->stack_index; + + // RUNLOG_ARTI("%s before expr term interpret %s %s\n", spaces+50-depth, key, value.as().c_str()); + interpret(value, nullptr, current_scope, depth + 1); //pushes results + + // RUNLOG_ARTI("%s %s interpret > (%u - %u = %u)\n", spaces+50-depth, key, valueStack->stack_index, oldIndex, valueStack->stack_index - oldIndex); + + // always 3, 5, 7 ... values + if (valueStack->stack_index - oldIndex >= 3) + { + float left = valueStack->floatStack[oldIndex]; + for (int i = 3; i <= valueStack->stack_index - oldIndex; i += 2) + { + uint8_t operatorx = valueStack->floatStack[oldIndex + i - 2]; + float right = valueStack->floatStack[oldIndex + i - 1]; + + float evaluation = 0; + + switch (operatorx) { + case F_plus: + evaluation = left + right; + break; + case F_minus: + evaluation = left - right; + break; + case F_multiplication: + evaluation = left * right; + break; + case F_division: { + if (right == 0) + { + right = 1; + ERROR_ARTI("%s division by 0 not possible, divisor ignored for %f\n", spaces+50-depth, left); + } + evaluation = left / right; + break; + } + case F_modulo: { + if (right == 0) { + evaluation = left; + ERROR_ARTI("%s mod 0 not possible, mod ignored %f\n", spaces+50-depth, left); + } + else + evaluation = fmod(left, right); + break; + } + case F_bitShiftLeft: + evaluation = (int)left << (int)right; //only works on integers + break; + case F_bitShiftRight: + evaluation = (int)left >> (int)right; //only works on integers + break; + case F_equal: + evaluation = left == right; + break; + case F_notEqual: + evaluation = left != right; + break; + case F_lessThen: + evaluation = left < right; + break; + case F_lessThenOrEqual: + evaluation = left <= right; + break; + case F_greaterThen: + evaluation = left > right; + break; + case F_greaterThenOrEqual: + evaluation = left >= right; + break; + case F_and: + evaluation = left && right; + break; + case F_or: + evaluation = left || right; + break; + default: + ERROR_ARTI("%s Programming error: unknown operator %u\n", spaces+50-depth, operatorx); + } + + RUNLOG_ARTI("%s %f %s %f = %f (pop %u, push %u)\n", spaces+50-depth, left, tokenToString(operatorx), right, evaluation, valueStack->stack_index - i, valueStack->stack_index - i + 1); + + left = evaluation; + } + + valueStack->stack_index = oldIndex; + + valueStack->push(left); + } + else if (valueStack->stack_index - oldIndex == 2) // unary: operator and 1 operand + { + uint8_t operatorx = valueStack->floatStack[oldIndex]; + if (operatorx == F_minus) + { + valueStack->stack_index = oldIndex; + valueStack->push(-valueStack->floatStack[oldIndex + 1]); + RUNLOG_ARTI("%s unary - %f (push %u)\n", spaces+50-depth, valueStack->floatStack[oldIndex + 1], valueStack->stack_index ); + } + else + RUNLOG_ARTI("%s unary operator not supported %u %s\n", spaces+50-depth, operatorx, tokenToString(operatorx)); + } + + visitedAlready = true; + break; + } + case F_For: + { + RUNLOG_ARTI("%s For (%u)\n", spaces+50-depth, valueStack->stack_index); + + interpret(value, "assign", current_scope, depth + 1); //creates the assignment + ActivationRecord* ar = this->callStack->peek(); + + bool continuex = true; + uint16_t counter = 0; + while (continuex && counter < 2000) //to avoid endless loops + { + RUNLOG_ARTI("%s iteration\n", spaces+50-depth); + + RUNLOG_ARTI("%s check to condition\n", spaces+50-depth); + interpret(value, "expr", current_scope, depth + 1); //pushes result of to + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s conditionResult (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) { //conditionResult is true + RUNLOG_ARTI("%s 1 => run block\n", spaces+50-depth); + interpret(value["block"], nullptr, current_scope, depth + 1); + + RUNLOG_ARTI("%s assign next value\n", spaces+50-depth); + interpret(value["increment"], nullptr, current_scope, depth + 1); //pushes increment result + // MEMORY_ARTI("%s Iteration %u %u\n", spaces+50-depth, counter, FREE_SIZE); + } + else + { + if (conditionResult == 0) { //conditionResult is false + RUNLOG_ARTI("%s 0 => end of For\n", spaces+50-depth); + continuex = false; + } + else // conditionResult is a value (e.g. in pascal) + { + //get the variable from assignment + float varValue = ar->getFloat(ar->lastSetIndex); + + float evaluation = varValue <= conditionResult; + RUNLOG_ARTI("%s %s.(%u) %f <= %f = %f\n", spaces+50-depth, ar->name, ar->lastSetIndex, varValue, conditionResult, evaluation); + + if (evaluation == 1) + { + RUNLOG_ARTI("%s 1 => run block\n", spaces+50-depth); + interpret(value["block"], nullptr, current_scope, depth + 1); + + //increment + ar->set(ar->lastSetIndex, varValue + 1); + } + else + { + RUNLOG_ARTI("%s 0 => end of For\n", spaces+50-depth); + continuex = false; + } + } + } + counter++; + }; + + if (continuex) + ERROR_ARTI("%s too many iterations in for loop %u\n", spaces+50-depth, counter); + + visitedAlready = true; + break; + } // case + case F_If: + { + RUNLOG_ARTI("%s If (stack %u)\n", spaces+50-depth, valueStack->stack_index); + + RUNLOG_ARTI("%s condition\n", spaces+50-depth); + if (value.containsKey("expr")) + interpret(value, "expr", current_scope, depth + 1); + // else if (value.containsKey("varref")) + // interpret(value, "varref", current_scope, depth + 1); + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) //conditionResult is true + interpret(value, "block", current_scope, depth + 1); + else + interpret(value, "elseBlock", current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + case F_Cex: + { + RUNLOG_ARTI("%s Cex (stack %u)\n", spaces+50-depth, valueStack->stack_index); + + RUNLOG_ARTI("%s condition\n", spaces+50-depth); + interpret(value, "expr", current_scope, depth + 1); + + float conditionResult = valueStack->popFloat(); + + RUNLOG_ARTI("%s (pop %u)\n", spaces+50-depth, valueStack->stack_index); + + if (conditionResult == 1) //conditionResult is true + interpret(value, "trueExpr", current_scope, depth + 1); + else + interpret(value, "falseExpr", current_scope, depth + 1); + + visitedAlready = true; + break; + } // case + default: //visitedalready false => recursive call + break; + } + } // is key is node_name + + if (!visitedAlready && value.size() > 0) // if size == 0 then injected key/value like operator + interpret(value, nullptr, current_scope, depth + 1); + } // if treeelement + // RUNLOG_ARTI("%s before end for %u\n", spaces+50-depth, depth); + } // for (JsonPair) + } + else if (parseTree.is()) + { + for (JsonVariant newParseTree: parseTree.as()) + { + // RUNLOG_ARTI("%s\n", spaces+50-depth, "Array ", parseTree[i], " "; + interpret(newParseTree, nullptr, current_scope, depth + 1); + } + } + else { //not array + ERROR_ARTI("%s Error: parseTree should be array or object %s (%u)\n", spaces+50-depth, parseTree.as().c_str(), depth); + } + + return !errorOccurred; + } //interpret + + void closeLog() + { + //non arduino stops log here + #if ARTI_PLATFORM == ARTI_ARDUINO + if (logToFile) + { + logFile.close(); + logToFile = false; + } + #else + if (logToFile) + { + fclose(logFile); + logToFile = false; + } + #endif + } + + bool setup(const char *definitionName, const char *programName) + { + errorOccurred = false; + frameCounter = 0; + + logToFile = true; + //open logFile + if (logToFile) + { + #if ARTI_PLATFORM == ARTI_ARDUINO + strcpy(logFileName, "/"); + #endif + strcpy(logFileName, programName); + strcat(logFileName, ".log"); + + #if ARTI_PLATFORM == ARTI_ARDUINO + logFile = LITTLEFS.open(logFileName,"w"); + #else + logFile = fopen (logFileName,"w"); + #endif + } + + MEMORY_ARTI("setup %u bytes free\n", FREE_SIZE); + + if (stages < 1) {close(); return true;} + bool loadParseTreeFile = false; + + #if ARTI_PLATFORM == ARTI_ARDUINO + File definitionFile; + definitionFile = LITTLEFS.open(definitionName, "r"); + #else + std::fstream definitionFile; + definitionFile.open(definitionName, std::ios::in); + #endif + + MEMORY_ARTI("open %s %u ✓\n", definitionName, FREE_SIZE); + + if (!definitionFile) + { + ERROR_ARTI("Definition file %s not found. Press Download wled.json\n", definitionName); + return false; + } + + //open definitionFile + #if ARTI_PLATFORM == ARTI_ARDUINO + definitionJsonDoc = new DynamicJsonDocument(8192); //currently 5335 + #else + definitionJsonDoc = new DynamicJsonDocument(16384); //currently 9521 + #endif + + // mandatory tokens: + // "ID": "ID", + // "INTEGER_CONST": "INTEGER_CONST", + // "REAL_CONST": "REAL_CONST", + + MEMORY_ARTI("definitionTree %u => %u ✓\n", (unsigned int)definitionJsonDoc->capacity(), FREE_SIZE); //unsigned int needed when running embedded to suppress warnings + + DeserializationError err = deserializeJson(*definitionJsonDoc, definitionFile); + if (err) + { + ERROR_ARTI("deserializeJson() of definition failed with code %s\n", err.c_str()); + return false; + } + definitionFile.close(); + definitionJson = definitionJsonDoc->as(); + + JsonObject::iterator objectIterator = definitionJson.begin(); + JsonObject metaData = objectIterator->value(); + const char * version = metaData["version"]; + if (strcmp(version, "0.3.1") != 0) + { + ERROR_ARTI("Version of definition.json file (%s) should be 0.3.1.\nPress Download wled.json\n", version); + return false; + } + const char * startNode = metaData["start"]; + if (startNode == nullptr) + { + ERROR_ARTI("Setup Error: No start node found in definition file %s\n", definitionName); + return false; + } + + #if ARTI_PLATFORM == ARTI_ARDUINO + File programFile; + programFile = LITTLEFS.open(programName, "r"); + #else + std::fstream programFile; + programFile.open(programName, std::ios::in); + #endif + MEMORY_ARTI("open %s %u ✓\n", programName, FREE_SIZE); + if (!programFile) + { + ERROR_ARTI("Program file %s not found\n", programName); + return false; + } + + //open programFile + char * programText; + uint16_t programFileSize; + #if ARTI_PLATFORM == ARTI_ARDUINO + programFileSize = programFile.size(); + programText = (char *)malloc(programFileSize+1); + programFile.read((byte *)programText, programFileSize); + programText[programFileSize] = '\0'; + #else + programText = (char *)malloc(programTextSize); + programFile.read(programText, programTextSize); + DEBUG_ARTI("programFile size %lu bytes\n", programFile.gcount()); + programText[programFile.gcount()] = '\0'; + programFileSize = strlen(programText); + #endif + programFile.close(); + + char parseTreeName[fileNameLength]; + strcpy(parseTreeName, programName); + // if (loadParseTreeFile) + // strcpy(parseTreeName, "Gen"); + strcat(parseTreeName, ".json"); + #if ARTI_PLATFORM == ARTI_ARDUINO + parseTreeJsonDoc = new DynamicJsonDocument(32768); //less memory on arduino: 32 vs 64 bit? + #else + parseTreeJsonDoc = new DynamicJsonDocument(65536); + #endif + + MEMORY_ARTI("parseTree %u => %u ✓\n", (unsigned int)parseTreeJsonDoc->capacity(), FREE_SIZE); + + //parse + + #ifdef ARTI_DEBUG // only read write file if debug is on + #if ARTI_PLATFORM == ARTI_ARDUINO + File parseTreeFile; + parseTreeFile = LITTLEFS.open(parseTreeName, loadParseTreeFile?"r":"w"); + #else + std::fstream parseTreeFile; + parseTreeFile.open(parseTreeName, loadParseTreeFile?std::ios::in:std::ios::out); + #endif + #endif + + if (stages < 1) {close(); return true;} + + if (!loadParseTreeFile) + { + parseTreeJson = parseTreeJsonDoc->as(); + + lexer = new Lexer(programText, definitionJson); + lexer->get_next_token(); + + if (stages < 2) {close(); return true;} + + uint8_t result = parse(parseTreeJson, startNode, '&', lexer->definitionJson[startNode], 0); + + if (this->lexer->pos != strlen(this->lexer->text)) + { + ERROR_ARTI("Node %s Program not entirely parsed (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + return false; + } + else if (result == ResultFail) + { + ERROR_ARTI("Node %s Program parsing failed (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + return false; + } + else + { + DEBUG_ARTI("Node %s Parsed until (%u,%u) %u of %u\n", startNode, this->lexer->lineno, this->lexer->column, this->lexer->pos, (unsigned int)strlen(this->lexer->text)); + MEMORY_ARTI("parse %u ✓\n", FREE_SIZE); + } + + MEMORY_ARTI("definitionTree %u / %u%% (%u %u %u)\n", (unsigned int)definitionJsonDoc->memoryUsage(), 100 * definitionJsonDoc->memoryUsage() / definitionJsonDoc->capacity(), (unsigned int)definitionJsonDoc->size(), definitionJsonDoc->overflowed(), (unsigned int)definitionJsonDoc->nesting()); + MEMORY_ARTI("parseTree %u / %u%% (%u %u %u)\n", (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->size(), parseTreeJsonDoc->overflowed(), (unsigned int)parseTreeJsonDoc->nesting()); + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + delete lexer; lexer = nullptr; + } + else + { + // read parseTree + #ifdef ARTI_DEBUG // only write file if debug is on + DeserializationError err = deserializeJson(*parseTreeJsonDoc, parseTreeFile); + if (err) + { + ERROR_ARTI("deserializeJson() of parseTree failed with code %s\n", err.c_str()); + return false; + } + #endif + } + #if ARTI_PLATFORM == ARTI_ARDUINO //not on windows as cause crash??? + free(programText); + #endif + + if (stages >= 3) + { + DEBUG_ARTI("\nOptimizer\n"); + if (!optimize(parseTreeJson)) + { + ERROR_ARTI("Optimize failed\n"); + return false; + } + else + MEMORY_ARTI("optimize %u ✓\n", FREE_SIZE); + + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + if (stages >= 4) + { + ANDBG_ARTI("\nAnalyzer\n"); + if (!analyze(parseTreeJson)) + { + ERROR_ARTI("Analyze failed\n"); + errorOccurred = true; + } + else + MEMORY_ARTI("analyze %u ✓\n", FREE_SIZE); + } + } + + size_t memBefore = parseTreeJsonDoc->memoryUsage(); + parseTreeJsonDoc->garbageCollect(); + MEMORY_ARTI("garbageCollect %u / %u%% -> %u / %u%%\n", (unsigned int)memBefore, 100 * memBefore / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity()); + + #ifdef ARTI_DEBUG // only write parseTree file if debug is on + if (!loadParseTreeFile) + serializeJsonPretty(*parseTreeJsonDoc, parseTreeFile); + parseTreeFile.close(); + #endif + + if (stages < 5 || errorOccurred) {close(); return !errorOccurred;} + + //interpret main + callStack = new CallStack(); + valueStack = new ValueStack(); + + if (global_scope != nullptr) //due to undefined functions??? wip + { + RUNLOG_ARTI("\ninterpret %s %u %u\n", global_scope->scope_name, global_scope->scope_level, global_scope->symbolsIndex); + + if (!interpret(parseTreeJson)) + { + ERROR_ARTI("Interpret main failed\n"); + return false; + } + } + else + { + ERROR_ARTI("\nInterpret global scope is nullptr\n"); + return false; + } + + MEMORY_ARTI("Interpret main %u ✓\n", FREE_SIZE); + + return !errorOccurred; + } // setup + + void close() { + MEMORY_ARTI("closing Arti %u\n", FREE_SIZE); + + if (callStack != nullptr) {delete callStack; callStack = nullptr;} + if (valueStack != nullptr) {delete valueStack; valueStack = nullptr;} + if (global_scope != nullptr) {delete global_scope; global_scope = nullptr;} + + if (definitionJsonDoc != nullptr) { + MEMORY_ARTI("definitionJson %u / %u%% (%u %u %u)\n", (unsigned int)definitionJsonDoc->memoryUsage(), 100 * definitionJsonDoc->memoryUsage() / definitionJsonDoc->capacity(), (unsigned int)definitionJsonDoc->size(), definitionJsonDoc->overflowed(), (unsigned int)definitionJsonDoc->nesting()); + delete definitionJsonDoc; definitionJsonDoc = nullptr; + } + + if (parseTreeJsonDoc != nullptr) { + MEMORY_ARTI("parseTree %u / %0u%% (%u %u %u)\n", (unsigned int)parseTreeJsonDoc->memoryUsage(), 100 * parseTreeJsonDoc->memoryUsage() / parseTreeJsonDoc->capacity(), (unsigned int)parseTreeJsonDoc->size(), parseTreeJsonDoc->overflowed(), (unsigned int)parseTreeJsonDoc->nesting()); + delete parseTreeJsonDoc; parseTreeJsonDoc = nullptr; + } + + MEMORY_ARTI("closed Arti %u ✓\n", FREE_SIZE); + + closeLog(); + + #if ARTI_PLATFORM == ARTI_ARDUINO + LITTLEFS.remove(logFileName); //cleanup the /edit folder a bit + #endif + } +}; //ARTI \ No newline at end of file diff --git a/usermods/customeffects/arti_wled.h b/usermods/customeffects/arti_wled.h new file mode 100644 index 00000000..70f4941b --- /dev/null +++ b/usermods/customeffects/arti_wled.h @@ -0,0 +1,590 @@ +/* + @title Arduino Real Time Interpreter (ARTI) + @file arti_wled_plugin.h + @version 0.3.1 + @date 20220818 + @author Ewoud Wijma + @repo https://github.com/ewoudwijma/ARTI + */ + +#pragma once + +// For testing porposes, definitions should not only run on Arduino but also on Windows etc. +// Because compiling on arduino takes seriously more time than on Windows. +// The plugin.h files replace native arduino calls by windows simulated calls (e.g. setPixelColor will become printf) + +#define ARTI_ARDUINO 1 +#define ARTI_EMBEDDED 2 +#ifdef ESP32 //ESP32 is set in wled context: small trick to set WLED context + #define ARTI_PLATFORM ARTI_ARDUINO // else on Windows/Linux/Mac... +#endif + +#if ARTI_PLATFORM == ARTI_ARDUINO + #include "arti.h" +#else + #include "../arti.h" + #include + #include + #include +#endif + +//make sure the numbers here correspond to the order in which these functions are defined in wled.json!! +enum Externals +{ + F_ledCount, + F_matrixWidth, + F_matrixHeight, + F_setPixelColor, + F_leds, + F_setPixels, + F_hsv, + F_rgbw, + + F_setRange, + F_fill, + F_colorBlend, + F_colorWheel, + F_colorFromPalette, + F_beatSin, + F_fadeToBlackBy, + F_iNoise, + F_fadeOut, + + F_counter, + F_segcolor, + F_speedSlider, + F_intensitySlider, + F_custom1Slider, + F_custom2Slider, + F_custom3Slider, + F_sampleAvg, + + F_shift, + F_circle2D, + + F_constrain, + F_map, + F_seed, + F_random, + F_sin, + F_cos, + F_abs, + F_min, + F_max, + F_floor, + + F_hour, + F_minute, + F_second, + F_millis, + + F_time, + F_triangle, + F_wave, + F_square, + F_clamp, + + F_printf +}; + +#if ARTI_PLATFORM != ARTI_ARDUINO + class WS2812FX { + public: + uint16_t matrixWidth = 16, matrixHeight = 16; + + uint16_t XY(uint16_t x, uint16_t y) + { + return x%matrixWidth + y%matrixHeight * matrixWidth; + } + + uint32_t millis() + { + return 1000; // no millis defined for non embedded yet + } + + float arti_external_function(uint8_t function, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull, float par4 = floatNull, float par5 = floatNull); + float arti_get_external_variable(uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + void arti_set_external_variable(float value, uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + }; //class WS2812FX + + WS2812FX strip = WS2812FX(); + + #define PI 3.141592654 + +#endif + +float ARTI::arti_external_function(uint8_t function, float par1, float par2, float par3, float par4, float par5) +{ + return strip.arti_external_function(function, par1, par2, par3, par4, par5); +} + +float ARTI::arti_get_external_variable(uint8_t variable, float par1, float par2, float par3) +{ + return strip.arti_get_external_variable(variable, par1, par2, par3); +} + +void ARTI::arti_set_external_variable(float value, uint8_t variable, float par1, float par2, float par3) +{ + strip.arti_set_external_variable(value, variable, par1, par2, par3); +} + +float WS2812FX::arti_external_function(uint8_t function, float par1, float par2, float par3, float par4, float par5) { + // MEMORY_ARTI("fun %d(%f, %f, %f)\n", function, par1, par2, par3); + #if ARTI_PLATFORM == ARTI_ARDUINO + switch (function) { + case F_setPixelColor: { + if (par3 == floatNull) + setPixelColor(((uint16_t)par1)%SEGLEN, (uint32_t)par2); + else + setPixelColorXY((uint16_t)par1, (uint16_t)par2, (uint32_t)par3); + return floatNull; + } + case F_setPixels: + // setPixels(leds); No action needed as allready stored via sPC + return floatNull; + case F_hsv: + { + CRGB color = CHSV(par1, par2, par3); + return RGBW32(color.r, color.g, color.b, 0); + } + case F_rgbw: + return RGBW32(par1, par2, par3, par4); + + case F_setRange: { + setRange((uint16_t)par1, (uint16_t)par2, (uint32_t)par3); + return floatNull; + } + case F_fill: { + fill((uint32_t)par1); + return floatNull; + } + case F_colorBlend: + return color_blend((uint32_t)par1, (uint32_t)par2, (uint16_t)par3); + case F_colorWheel: + return SEGMENT.color_wheel((uint8_t)par1); + case F_colorFromPalette: + { + CRGB color = ColorFromPalette(SEGPALETTE, (uint8_t)par1, (uint8_t)par2, LINEARBLEND); + return RGBW32(color.r, color.g, color.b, 0); + } + case F_beatSin: + return beatsin8((uint8_t)par1, (uint8_t)par2, (uint8_t)par3, (uint8_t)par4, (uint8_t)par5); + case F_fadeToBlackBy: + SEGMENT.fadeToBlackBy((uint8_t)par1); + return floatNull; + case F_iNoise: + return inoise16((uint32_t)par1, (uint32_t)par2); + case F_fadeOut: + SEGMENT.fade_out((uint8_t)par1); + return floatNull; + + case F_segcolor: + return SEGCOLOR((uint8_t)par1); + + case F_shift: { + uint32_t saveFirstPixel = getPixelColor(0); + for (uint16_t i=0; i3.5->4) + int x = round(round((sin(radians(par1)) * halfLength + halfLength) * 10)/10) + deltaWidth; + int y = round(round((halfLength - cos(radians(par1)) * halfLength) * 10)/10) + deltaHeight; + return SEGMENT.XY(x,y); + } + + case F_constrain: + return constrain(par1, par2, par3); + case F_map: + return map(par1, par2, par3, par4, par5); + case F_seed: + random16_set_seed((uint16_t)par1); + return floatNull; + case F_random: + return random16(); + + case F_millis: + return millis(); + + default: {} + } + #else // not arduino + switch (function) + { + case F_setPixelColor: + PRINT_ARTI("%s(%f, %f, %f)\n", "setPixelColor", par1, par2, par3); + return floatNull; + case F_setPixels: + PRINT_ARTI("%s\n", "setPixels(leds)"); + return floatNull; + case F_hsv: + PRINT_ARTI("%s(%f, %f, %f)\n", "hsv", par1, par2, par3); + return par1 + par2 + par3; + case F_rgbw: + PRINT_ARTI("%s(%f, %f, %f, %f)\n", "rgbw", par1, par2, par3, par4); + return par1 + par2 + par3 + par4; + + case F_setRange: + return par1 + par2 + par3; + case F_fill: + PRINT_ARTI("%s(%f)\n", "fill", par1); + return floatNull; + case F_colorBlend: + return par1 + par2 + par3; + case F_colorWheel: + return par1; + case F_colorFromPalette: + return par1 + par2; + case F_beatSin: + return par1+par2+par3+par4+par5; + case F_fadeToBlackBy: + return par1; + case F_iNoise: + return par1 + par2; + case F_fadeOut: + return par1; + + case F_segcolor: + return par1; + + case F_shift: + PRINT_ARTI("%s(%f)\n", "shift", par1); + return floatNull; + case F_circle2D: + PRINT_ARTI("%s(%f)\n", "circle2D", par1); + return par1 / 2; + + case F_constrain: + return par1 + par2 + par3; + case F_map: + return par1 + par2 + par3 + par4 + par5; + case F_seed: + PRINT_ARTI("%s(%f)\n", "seed", par1); + return floatNull; + case F_random: + return rand(); + + case F_millis: + return 1000; + } + #endif + + //same on Arduino or Windows + switch (function) + { + case F_sin: + return sin(par1); + case F_cos: + return cos(par1); + case F_abs: + return fabs(par1); + case F_min: + return fmin(par1, par2); + case F_max: + return fmax(par1, par2); + case F_floor: + return floorf(par1); + + // Reference: https://github.com/atuline/PixelBlaze + case F_time: // A sawtooth waveform between 0.0 and 1.0 that loops about every 65.536*interval seconds. e.g. use .015 for an approximately 1 second. + { + float myVal = millis(); + myVal = myVal / 65535 / par1; // PixelBlaze uses 1000/65535 = .015259. + myVal = fmod(myVal, 1.0); // ewowi: with 0.015 as input, you get fmod(millis/1000,1.0), which has a period of 1 second, sounds right + return myVal; + } + case F_triangle: // Converts a sawtooth waveform v between 0.0 and 1.0 to a triangle waveform between 0.0 to 1.0. v "wraps" between 0.0 and 1.0. + return 1.0 - fabs(fmod(2 * par1, 2.0) - 1.0); + case F_wave: // Converts a sawtooth waveform v between 0.0 and 1.0 to a sinusoidal waveform between 0.0 to 1.0. Same as (1+sin(v*PI2))/2 but faster. v "wraps" between 0.0 and 1.0. + return (1 + sin(par1 * 2 * PI)) / 2; + case F_square: // Converts a sawtooth waveform v to a square wave using the provided duty cycle where duty is a number between 0.0 and 1.0. v "wraps" between 0.0 and 1.0. + { + float sinValue = arti_external_function(F_wave, par1); + return sinValue >= par2 ? 1 : 0; + } + case F_clamp: + { + const float t = par1 < par2 ? par2 : par1; + return t > par3 ? par3 : t; + } + + case F_printf: { + if (par3 == floatNull) { + if (par2 == floatNull) { + PRINT_ARTI("%f\n", par1); + } + else + PRINT_ARTI("%f, %f\n", par1, par2); + } + else + PRINT_ARTI("%f, %f, %f\n", par1, par2, par3); + return floatNull; + } + } + + ERROR_ARTI("Error: arti_external_function: %u not implemented\n", function); + errorOccurred = true; + return function; +} + +float WS2812FX::arti_get_external_variable(uint8_t variable, float par1, float par2, float par3) { + // MEMORY_ARTI("get %d(%f, %f, %f)\n", variable, par1, par2, par3); + #if ARTI_PLATFORM == ARTI_ARDUINO + switch (variable) + { + case F_ledCount: + return SEGLEN; + case F_matrixWidth: + return strip.matrixWidth; + case F_matrixHeight: + return strip.matrixHeight; + case F_leds: + if (par1 == floatNull) { + ERROR_ARTI("arti_get_external_variable leds without indices not supported yet (get leds)\n"); + errorOccurred = true; + return floatNull; + } + else if (par2 == floatNull) + return SEGMENT.getPixelColor((uint16_t)par1); + else + return SEGMENT.getPixelColorXY((uint16_t)par1, (uint16_t)par2); //2D value!! + + case F_counter: + return SEGENV.call; + case F_speedSlider: + return SEGMENT.speed; + case F_intensitySlider: + return SEGMENT.intensity; + case F_custom1Slider: + return SEGMENT.custom1; + case F_custom2Slider: + return SEGMENT.custom2; + case F_custom3Slider: + return SEGMENT.custom3; + case F_sampleAvg: + { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? + + return volumeSmth; + } + case F_hour: + return ((float)hour(localTime)); + case F_minute: + return ((float)minute(localTime)); + case F_second: + return ((float)second(localTime)); + } + #else + switch (variable) + { + case F_ledCount: + return 3; // used in testing e.g. for i = 1 to ledCount + case F_matrixWidth: + return 2; + case F_matrixHeight: + return 4; + case F_leds: + if (par1 == floatNull) { + ERROR_ARTI("arti_get_external_variable leds without indices not supported yet (get leds)\n"); + errorOccurred = true; + return F_leds; + } + else if (par2 == floatNull) + return par1; + else + return par1 * par2; //2D value!! + + case F_counter: + return frameCounter; + case F_speedSlider: + return F_speedSlider; + case F_intensitySlider: + return F_intensitySlider; + case F_custom1Slider: + return F_custom1Slider; + case F_custom2Slider: + return F_custom2Slider; + case F_custom3Slider: + return F_custom3Slider; + case F_sampleAvg: + return F_sampleAvg; + + case F_hour: + return F_hour; + case F_minute: + return F_minute; + case F_second: + return F_second; + } + #endif + + ERROR_ARTI("Error: arti_get_external_variable: %u not implemented\n", variable); + errorOccurred = true; + return variable; +} + +bool ledsSet; //check if leds is set + +void WS2812FX::arti_set_external_variable(float value, uint8_t variable, float par1, float par2, float par3) { + #if ARTI_PLATFORM == ARTI_ARDUINO + // MEMORY_ARTI("%s %s %u %u (%u)\n", spaces+50-depth, variable_name, par1, par2, esp_get_free_heap_size()); + switch (variable) + { + case F_leds: + if (par1 == floatNull) + { + ERROR_ARTI("arti_set_external_variable leds without indices not supported yet (set leds to %f)\n", value); + errorOccurred = true; + } + else if (par2 == floatNull) + SEGMENT.setPixelColor((uint16_t)par1%SEGLEN, value); + else + SEGMENT.setPixelColorXY((uint16_t)par1%SEGMENT.virtualWidth(), (uint16_t)par2%SEGMENT.virtualHeight(), value); //2D value!! + + ledsSet = true; + return; + } + #else + switch (variable) + { + case F_leds: + if (par1 == floatNull) + { + ERROR_ARTI("arti_set_external_variable leds without indices not supported yet (set leds to %f)\n", value); + errorOccurred = true; + } + else if (par2 == floatNull) + RUNLOG_ARTI("arti_set_external_variable: leds(%f) := %f\n", par1, value); + else + RUNLOG_ARTI("arti_set_external_variable: leds(%f, %f) := %f\n", par1, par2, value); + + ledsSet = true; + return; + } + #endif + + ERROR_ARTI("Error: arti_set_external_variable: %u not implemented\n", variable); + errorOccurred = true; +} //arti_set_external_variable + +bool ARTI::loop() +{ + if (stages < 5) {close(); return true;} + + if (parseTreeJsonDoc == nullptr || parseTreeJsonDoc->isNull()) + { + ERROR_ARTI("Loop: No parsetree created\n"); + errorOccurred = true; + return false; + } + else + { + uint8_t depth = 8; + + bool foundRenderFunction = false; + + const char * function_name = "renderFrame"; + Symbol* function_symbol = global_scope->lookup(function_name); + + ledsSet = false; + + if (function_symbol != nullptr) { //calling undefined function: pre-defined functions e.g. print + + foundRenderFunction = true; + + ActivationRecord* ar = new ActivationRecord(function_name, "Function", function_symbol->scope_level + 1); + + RUNLOG_ARTI("%s %s %s (%u)\n", spaces+50-depth, "Call", function_name, this->callStack->recordsCounter); + + this->callStack->push(ar); + + if (!interpret(function_symbol->block, nullptr, global_scope, depth + 1)) + return false; + + this->callStack->pop(); + + delete ar; ar = nullptr; + + } //function_symbol != nullptr + + function_name = "renderLed"; + function_symbol = global_scope->lookup(function_name); + + if (function_symbol != nullptr) { //calling undefined function: pre-defined functions e.g. print + + foundRenderFunction = true; + + ActivationRecord* ar = new ActivationRecord(function_name, "function", function_symbol->scope_level + 1); + + for (int i = 0; i< arti_get_external_variable(F_ledCount); i++) + { + ar->set(function_symbol->function_scope->symbols[0]->scope_index, i%strip.matrixWidth); // set x + if (function_symbol->function_scope->nrOfFormals == 2) // 2D + ar->set(function_symbol->function_scope->symbols[1]->scope_index, i/strip.matrixWidth); // set y + + this->callStack->push(ar); + + if (!interpret(function_symbol->block, nullptr, global_scope, depth + 1)) + return false; + + this->callStack->pop(); + } + + delete ar; ar = nullptr; + + } + + // if leds has been set during interpret(renderLed) + if (ledsSet) { + // Serial.println("ledsSet"); + arti_external_function(F_setPixels); + } + // else + // Serial.println("not ledsSet"); + + if (!foundRenderFunction) + { + ERROR_ARTI("%s renderFrame or renderLed not found\n", spaces+50-depth); + errorOccurred = true; + return false; + } + } + frameCounter++; + + if (frameCounter == 1) + startMillis = millis(); + + if (millis() - startMillis > 3000) //startMillis != 0 && logToFile && + { + // ERROR_ARTI("time %u\n", millis() - startMillis); + closeLog(); + // startMillis = 0; + } + + return true; +} // loop + +#if ARTI_PLATFORM == ARTI_ARDUINO + +ARTI * arti; + +#endif \ No newline at end of file diff --git a/usermods/customeffects/usermod_v2_customeffects.h b/usermods/customeffects/usermod_v2_customeffects.h new file mode 100644 index 00000000..891b4891 --- /dev/null +++ b/usermods/customeffects/usermod_v2_customeffects.h @@ -0,0 +1,199 @@ +#pragma once + +#include "wled.h" + +#include "arti.h" +#include "arti_wled.h" + +//declare weathermod global variables (always precede with weather_ (psuedo class static variables) +static uint32_t usermods_pushLoop = 0; //effect pushes loop to execute. might be interesting for audioreactive too + + +//effect function +uint16_t mode_customEffect(void) { + //tbd: move statics to SEGMENT.data + static bool succesful; + static bool notEnoughHeap; + + static char previousEffect[charLength]; + if (SEGENV.call == 0) + strcpy(previousEffect, ""); //force init + + char currentEffect[charLength]; + strcpy(currentEffect, (SEGMENT.name != nullptr)?SEGMENT.name:"default"); //note: switching preset with segment name to preset without does not clear the SEGMENT.name variable, but not gonna solve here ;-) + + if (strcmp(previousEffect, currentEffect) != 0) + { + strcpy(previousEffect, currentEffect); + + // if (artiWrapper != nullptr && artiWrapper->arti != nullptr) { + if (arti != nullptr) + { + arti->close(); + delete arti; arti = nullptr; + } + + // if (!SEGENV.allocateData(sizeof(ArtiWrapper))) return mode_static(); // We use this method for allocating memory for static variables. + // artiWrapper = reinterpret_cast(SEGENV.data); + arti = new ARTI(); + + char programFileName[fileNameLength]; + strcpy(programFileName, "/"); + strcat(programFileName, currentEffect); + strcat(programFileName, ".wled"); + + succesful = arti->setup("/wled.json", programFileName); + + if (!succesful) + ERROR_ARTI("Setup not succesful\n"); + } + else + { + if (succesful) // && SEGENV.call < 250 for each frame + { + if (esp_get_free_heap_size() <= 20000) + { + ERROR_ARTI("Not enough free heap (%u <= 30000)\n", esp_get_free_heap_size()); + notEnoughHeap = true; + succesful = false; + } + else + { + // static int previousMillis; + // static int previousCall; + // if (millis() - previousMillis > 5000) { //tried SEGENV.aux0 but that looks to be overwritten!!! (dangling pointer???) + // previousMillis = millis(); + // MEMORY_ARTI("Heap renderFrame %u %u fps\n", esp_get_free_heap_size(), (SEGENV.call - previousCall)/5); + // previousCall = SEGENV.call; + // } + + succesful = arti->loop(); + } + } + else + { + arti->closeLog(); + if (notEnoughHeap && esp_get_free_heap_size() > 20000) { + ERROR_ARTI("Again enough free heap, restart effect (%u > 30000)\n", esp_get_free_heap_size()); + succesful = true; + notEnoughHeap = false; + strcpy(previousEffect, ""); // force new create + } + else { + //mode_static + SEGMENT.fill(SEGCOLOR(0)); + return 350; + } + } + } + + return FRAMETIME; +} + +static const char _data_FX_MODE_CUSTOMEFFECT[] PROGMEM = " ⚙️ Custom Effect@Speed,Intensity,Custom 1, Custom 2, Custom 3;!;!"; + +class CustomEffectsUserMod : public Usermod { + private: + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; //usermod name + + unsigned long lastTime = 0; //will be used to download new forecast every hour + char errorMessage[100] = ""; + + public: + + void setup() { + strip.addEffect(255, &mode_customEffect, _data_FX_MODE_CUSTOMEFFECT); + } + + void connected() { + } + + void loop() { + //execute only if effect pushes it or every hour + if (usermods_pushLoop > millis() - 1000 && (lastTime == 0 || millis() - lastTime > 3600 * 1000)) { + lastTime = millis(); + } + usermods_pushLoop = 0; + } + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject& root) + { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + infoArr.add(errorMessage); //value + // infoArr.add(""); //unit + } + + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + //root["user0"] = userVar0; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + // userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value + //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); + } + + + void addToConfig(JsonObject& root) + { + } + + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + + + // * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + return configComplete; + } + + void appendConfigData() + { + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_CUSTOMEFFECTS; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char CustomEffectsUserMod::_name[] PROGMEM = "Custom Effects"; diff --git a/wled00/FX.h b/wled00/FX.h index c7f0d9b7..df34d0fb 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -634,6 +634,8 @@ typedef struct Segment { } segment; //static int segSize = sizeof(Segment); +#define floatNull -32768 //WLEDSR Custom Effects + // main "strip" class class WS2812FX { // 96 bytes typedef uint16_t (*mode_ptr)(void); // pointer to mode function @@ -898,6 +900,12 @@ class WS2812FX { // 96 bytes void estimateCurrentAndLimitBri(void); + + public: + //WLEDSR Custom Effects + float arti_external_function(uint8_t function, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull, float par4 = floatNull, float par5 = floatNull); + float arti_get_external_variable(uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); + void arti_set_external_variable(float value, uint8_t variable, float par1 = floatNull, float par2 = floatNull, float par3 = floatNull); }; extern const char JSON_mode_names[]; diff --git a/wled00/const.h b/wled00/const.h index 377e7b35..8ca505cf 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -80,6 +80,7 @@ #define USERMOD_ID_SI7021_MQTT_HA 29 //Usermod "usermod_si7021_mqtt_ha.h" #define USERMOD_ID_BME280 30 //Usermod "usermod_bme280.h #define USERMOD_ID_AUDIOREACTIVE 31 //Usermod "audioreactive.h" +#define USERMOD_ID_CUSTOMEFFECTS 32 //Usermod "usermod_v2_customeffects.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 7738a1e9..07de8f46 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -139,6 +139,10 @@ #include "../usermods/audioreactive/audio_reactive.h" #endif +#ifdef USERMOD_CUSTOMEFFECTS +#include "../usermods/customeffects/usermod_v2_customeffects.h" +#endif + void registerUsermods() { /* @@ -265,4 +269,9 @@ void registerUsermods() #endif usermods.add(new AudioReactive()); #endif + + #ifdef USERMOD_CUSTOMEFFECTS + usermods.add(new CustomEffectsUserMod()); + #endif + } From 068089751db480a22b2b0211f37df20397bd7ca7 Mon Sep 17 00:00:00 2001 From: Ewowi Date: Tue, 30 Aug 2022 12:55:01 +0200 Subject: [PATCH 2/4] Add CustomEffects as usermod: step 2: ui works --- usermods/customeffects/customeffects.css | 12 + usermods/customeffects/customeffects.js | 154 + .../customeffects/usermod_v2_customeffects.h | 16 +- wled00/data/index.htm | 7 + wled00/data/index.js | 6 + wled00/html_ui.h | 3803 +++++++++-------- wled00/json.cpp | 6 + 7 files changed, 2130 insertions(+), 1874 deletions(-) create mode 100644 usermods/customeffects/customeffects.css create mode 100644 usermods/customeffects/customeffects.js diff --git a/usermods/customeffects/customeffects.css b/usermods/customeffects/customeffects.css new file mode 100644 index 00000000..39ef3967 --- /dev/null +++ b/usermods/customeffects/customeffects.css @@ -0,0 +1,12 @@ +.ceTextarea { + width: 90%; + height: 300px; + resize: none; + white-space: pre; +} + +#kceEditor { + max-width: 490px; + display: inline-block; +} + diff --git a/usermods/customeffects/customeffects.js b/usermods/customeffects/customeffects.js new file mode 100644 index 00000000..78d7cf79 --- /dev/null +++ b/usermods/customeffects/customeffects.js @@ -0,0 +1,154 @@ + +var isCEEditor = false; + +function toggleCEEditor(name, segID) { + if (isInfo) toggleInfo(); + if (isNodes) toggleNodes(); + isCEEditor = !isCEEditor; + if (isCEEditor) populateCEEditor(name, segID); + d.getElementById('ceEditor').style.transform = (isCEEditor) ? "translateY(0px)":"translateY(100%)"; +} + +function fetchAndExecute(url, name, callback) +{ + fetch + (url+name, { + method: 'get' + }) + .then(res => { + if (!res.ok) { + showToast("File " + name + " not found", true); + return ""; + } + return res.text(); + }) + .then(text => { + callback(text); + }) + .catch(function (error) { + showToast("Error getting " + name, true); + // showToast(error, true); + // console.log(error); + presetError(false); + }) + .finally(() => { + // if (callback) setTimeout(callback,99); + }); +} + +function loadLogFile(name, attempt) { + var ceLogArea = d.getElementById("ceLogArea"); + fetchAndExecute((loc?`http://${locip}`:'.') + "/", name , function(logtext) + { + if (logtext == "") { + if (attempt < 10) { + ceLogArea.value = ("...........").substring(0, attempt + 1); + setTimeout(() => + { + loadLogFile(name, attempt + 1); + }, 1000); + } + else + ceLogArea.value = "log not found after 10 seconds"; + } + else + ceLogArea.value = logtext; + }); +} + +function saveCE(name, segID) { + showToast("Saving " + name); + + var ceProgramArea = d.getElementById("ceProgramArea"); + + uploadFileWithText("/" + name, ceProgramArea.value); + + var obj = {"seg": {"id": segID, "reset": true}}; + requestJson(obj); + + var ceLogArea = d.getElementById("ceLogArea"); + ceLogArea.value = "."; + setTimeout(() => + { + loadLogFile(name + ".log", 1); + }, 1000); +} + +function populateCEEditor(name, segID) +{ + fetchAndExecute((loc?`http://${locip}`:'.') + "/", name + ".wled", function(text) + { + var cn=`Custom Effects Editor
+ ${name}.wled
+
+ +
+ +
+ +
+ Custom Effects Library
+ Custom Effects Help
+
Compile and Run Log
+
+ Run log > 3 seconds is send to Serial Ouput.`; + + d.getElementById('kceEditor').innerHTML = cn; + + var ceLogArea = d.getElementById("ceLogArea"); + ceLogArea.value = "."; + loadLogFile(name + ".wled.log", 1); + + }); +} + +function downloadCEFile(name) { + var url = "https://raw.githubusercontent.com/MoonModules/WLED-Effects/master/CustomEffects/wled/"; + + fetchAndExecute(url, name, function(text) { + console.log(text); + if (name == "wled.json" || name == "presets.json") { + if (!confirm('Are you sure to download/overwrite ' + name + '?')) + return; + uploadFileWithText("/" + name, text); + } + else + { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = text; + } + }); + + return; + + var request = new XMLHttpRequest(); + request.onload = function() { + if (name == "wled.json" || name == "presets.json") { + if (!confirm('Are you sure to download ' + name + '?')) + return; + uploadFileWithText("/" + name, request.response); + } + else + { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = request.response; + } + } + request.open("GET", url); + request.send(); + } + +function loadCETemplate(name) { + var ceProgramArea = d.getElementById("ceProgramArea"); + ceProgramArea.value = `/* + Custom Effects Template + */ + program ${name} + { + function renderFrame() + { + setPixelColor(counter, colorFromPalette(counter, counter)) + } + }`; + +} \ No newline at end of file diff --git a/usermods/customeffects/usermod_v2_customeffects.h b/usermods/customeffects/usermod_v2_customeffects.h index 891b4891..389fa8ac 100644 --- a/usermods/customeffects/usermod_v2_customeffects.h +++ b/usermods/customeffects/usermod_v2_customeffects.h @@ -100,10 +100,16 @@ class CustomEffectsUserMod : public Usermod { unsigned long lastTime = 0; //will be used to download new forecast every hour char errorMessage[100] = ""; + bool enabled = false; + bool initDone = false; + public: void setup() { - strip.addEffect(255, &mode_customEffect, _data_FX_MODE_CUSTOMEFFECT); + if (!initDone) + strip.addEffect(255, &mode_customEffect, _data_FX_MODE_CUSTOMEFFECT); + initDone = true; + enabled = true; } void connected() { @@ -140,6 +146,12 @@ class CustomEffectsUserMod : public Usermod { void addToJsonState(JsonObject& root) { //root["user0"] = userVar0; + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; } @@ -196,4 +208,4 @@ class CustomEffectsUserMod : public Usermod { }; // strings to reduce flash memory usage (used more than twice) -const char CustomEffectsUserMod::_name[] PROGMEM = "Custom Effects"; +const char CustomEffectsUserMod::_name[] PROGMEM = "CustomEffects"; diff --git a/wled00/data/index.htm b/wled00/data/index.htm index f4b50c44..c9ddeaa3 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -51,6 +51,7 @@ setTimeout(()=>{h.appendChild(l)},100); + @@ -365,6 +366,11 @@ + + + @@ -380,5 +386,6 @@
+ diff --git a/wled00/data/index.js b/wled00/data/index.js index d9e3c3c6..5fedc024 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -684,7 +684,11 @@ function populateSegments(s) let li = lastinfo; segCount = 0; lowestUnused = 0; lSeg = 0; + console.log(s); + for (var inst of (s.seg||[])) { + console.log(inst); + segCount++; let i = parseInt(inst.id); @@ -728,6 +732,7 @@ function populateSegments(s) `; + let cusEff = `
`; cn += `