/* xdrv_52_9_berry.ino - Berry scripting language Copyright (C) 2021 Stephan Hadinger, Berry language by Guan Wenliang https://github.com/Skiars/berry This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_BERRY #define XDRV_52 52 #include extern "C" { #include "be_bytecode.h" #include "be_var.h" } #include "berry_tasmota.h" #ifdef USE_MATTER_DEVICE #include "berry_matter.h" #endif #ifdef USE_WS2812 #include "berry_animate.h" #endif #include "be_vm.h" #include "ZipReadFS.h" #include "ccronexpr.h" #include "berry_custom.h" extern "C" { extern void be_load_custom_libs(bvm *vm); extern void be_tracestack(bvm *vm); } const char kBrCommands[] PROGMEM = D_PRFX_BR "|" // prefix D_CMND_BR_RUN "|" D_CMND_BR_RESTART ; void (* const BerryCommand[])(void) PROGMEM = { CmndBrRun, CmndBrRestart }; int32_t callBerryEventDispatcher(const char *type, const char *cmd, int32_t idx, const char *payload, uint32_t data_len = 0); // // Sanity Check for be_top() // // Checks that the Berry stack is empty, if not print a Warning and empty it // void checkBeTop(void) { int32_t top = be_top(berry.vm); if (top != 0) { be_pop(berry.vm, top); // TODO should not be there AddLog(LOG_LEVEL_DEBUG, D_LOG_BERRY "Error be_top is non zero=%d", top); } } /*********************************************************************************************\ * Memory handler * Use PSRAM if available \*********************************************************************************************/ extern "C" { void *berry_malloc(size_t size); void *berry_realloc(void *ptr, size_t size); #ifdef USE_BERRY_PSRAM void *berry_malloc(size_t size) { return special_malloc(size); } void *berry_realloc(void *ptr, size_t size) { return special_realloc(ptr, size); } void *berry_calloc(size_t num, size_t size) { return special_calloc(num, size); } #else void *berry_malloc(size_t size) { return malloc(size); } void *berry_realloc(void *ptr, size_t size) { return realloc(ptr, size); } void *berry_calloc(size_t num, size_t size) { return calloc(num, size); } #endif // USE_BERRY_PSRAM void *berry_malloc32(size_t size) { #ifdef USE_BERRY_IRAM return special_malloc32(size); #else return NULL; /* return NULL to indicate that IRAM is not enabled */ #endif } void berry_free(void *ptr) { free(ptr); } } /*********************************************************************************************\ * Handlers for Berry calls and async * \*********************************************************************************************/ // // call a function (if exists) of type void -> void // If event == nullptr, then take XdrvMailbox.data bool callBerryRule(const char *event, bool teleperiod) { bool save_rules_busy = berry.rules_busy; bool exec_rule = !save_rules_busy; // true if the rule is executed, false if we only record the value // if (berry.rules_busy) { return false; } berry.rules_busy = true; bool serviced = false; serviced = callBerryEventDispatcher(teleperiod ? "tele" : "rule", nullptr, exec_rule, event ? event : XdrvMailbox.data); berry.rules_busy = save_rules_busy; return serviced; // TODO event not handled } size_t callBerryGC(void) { return callBerryEventDispatcher(PSTR("gc"), nullptr, 0, nullptr); } // call the event dispatcher from Tasmota object // if data_len is non-zero, the event is also sent as raw `bytes()` object because the string may lose data int32_t callBerryEventDispatcher(const char *type, const char *cmd, int32_t idx, const char *payload, uint32_t data_len) { int32_t ret = 0; bvm *vm = berry.vm; if (nullptr == vm) { return ret; } checkBeTop(); be_getglobal(vm, PSTR("tasmota")); if (!be_isnil(vm, -1)) { be_getmethod(vm, -1, PSTR("event")); if (!be_isnil(vm, -1)) { be_pushvalue(vm, -2); // add instance as first arg be_pushstring(vm, type != nullptr ? type : ""); be_pushstring(vm, cmd != nullptr ? cmd : ""); be_pushint(vm, idx); be_pushstring(vm, payload != nullptr ? payload : ""); // empty json BrTimeoutStart(); if (data_len > 0) { be_pushbytes(vm, payload, data_len); // if data_len is set, we also push raw bytes ret = be_pcall(vm, 6); // 6 arguments be_pop(vm, 1); } else { ret = be_pcall(vm, 5); // 5 arguments } BrTimeoutReset(); if (ret != 0) { be_error_pop_all(berry.vm); // clear Berry stack return ret; } be_pop(vm, 5); if (be_isint(vm, -1) || be_isbool(vm, -1)) { if (be_isint(vm, -1)) { ret = be_toint(vm, -1); } if (be_isbool(vm, -1)) { ret = be_tobool(vm, -1); } } } be_pop(vm, 1); // remove method } be_pop(vm, 1); // remove instance object checkBeTop(); return ret; } // Simplified version of event loop. Just call `tasmota.fast_loop()` // `every_5ms` is a flag to wait at least 5ms between calss to `tasmota.fast_loop()` void callBerryFastLoop(bool every_5ms) { static uint32_t fast_loop_last_call = 0; bvm *vm = berry.vm; if (nullptr == vm) { return; } uint32_t now = millis(); if (every_5ms) { if (!TimeReached(fast_loop_last_call + USE_BERRY_FAST_LOOP_SLEEP_MS /* 5ms */)) { return; } } fast_loop_last_call = now; // TODO - can we make this dereferencing once for all? if (be_getglobal(vm, "tasmota")) { if (be_getmethod(vm, -1, "fast_loop")) { be_pushvalue(vm, -2); // add instance as first arg BrTimeoutStart(); int32_t ret = be_pcall(vm, 1); if (ret != 0) { be_error_pop_all(berry.vm); // clear Berry stack } BrTimeoutReset(); be_pop(vm, 1); } be_pop(vm, 1); // remove method } be_pop(vm, 1); // remove instance object be_pop(vm, be_top(vm)); // clean } /*********************************************************************************************\ * VM Observability \*********************************************************************************************/ void BerryObservability(bvm *vm, int event...); void BerryObservability(bvm *vm, int event...) { va_list param; va_start(param, event); static int32_t vm_usage = 0; static uint32_t gc_time = 0; switch (event) { case BE_OBS_PCALL_ERROR: // error after be_pcall { int32_t top = be_top(vm); // check if we have two strings for an Exception if (top >= 2 && be_isstring(vm, -1) && be_isstring(vm, -2)) { berry_log_C(PSTR(D_LOG_BERRY "Exception> '%s' - %s"), be_tostring(berry.vm, -2), be_tostring(berry.vm, -1)); be_tracestack(vm); } else { be_dumpstack(vm); } } case BE_OBS_GC_START: { gc_time = millis(); vm_usage = va_arg(param, int32_t); } break; case BE_OBS_GC_END: { int32_t vm_usage2 = va_arg(param, int32_t); uint32_t gc_elapsed = millis() - gc_time; uint32_t vm_scanned = va_arg(param, uint32_t); uint32_t vm_freed = va_arg(param, uint32_t); size_t slots_used_before_gc = va_arg(param, size_t); size_t slots_allocated_before_gc = va_arg(param, size_t); size_t slots_used_after_gc = va_arg(param, size_t); size_t slots_allocated_after_gc = va_arg(param, size_t); AddLog(LOG_LEVEL_DEBUG_MORE, D_LOG_BERRY "GC from %i to %i bytes, objects freed %i/%i (in %d ms) - slots from %i/%i to %i/%i", vm_usage, vm_usage2, vm_freed, vm_scanned, gc_elapsed, slots_used_before_gc, slots_allocated_before_gc, slots_used_after_gc, slots_allocated_after_gc); #ifdef UBE_BERRY_DEBUG_GC // Add more in-deptch metrics AddLog(LOG_LEVEL_DEBUG_MORE, D_LOG_BERRY "GC timing (us) 1:%i 2:%i 3:%i 4:%i 5:%i total:%i", vm->micros_gc1 - vm->micros_gc0, vm->micros_gc2 - vm->micros_gc1, vm->micros_gc3 - vm->micros_gc2, vm->micros_gc4 - vm->micros_gc3, vm->micros_gc5 - vm->micros_gc4, vm->micros_gc5 - vm->micros_gc0 ); AddLog(LOG_LEVEL_DEBUG_MORE, D_LOG_BERRY "GC by type " "string:%i class:%i proto:%i instance:%i map:%i " "list:%i closure:%i ntvclos:%i module:%i comobj:%i", vm->gc_mark_string, vm->gc_mark_class, vm->gc_mark_proto, vm->gc_mark_instance, vm->gc_mark_map, vm->gc_mark_list, vm->gc_mark_closure, vm->gc_mark_ntvclos, vm->gc_mark_module, vm->gc_mark_comobj ); #endif // make new threshold tighter when we reach high memory usage if (!UsePSRAM() && vm->gc.threshold > 20*1024) { vm->gc.threshold = vm->gc.usage + 10*1024; // increase by only 10 KB } } break; case BE_OBS_STACK_RESIZE_START: { int32_t stack_before = va_arg(param, int32_t); int32_t stack_after = va_arg(param, int32_t); AddLog(LOG_LEVEL_DEBUG_MORE, PSTR(D_LOG_BERRY "Stack resized from %i to %i bytes"), stack_before, stack_after); } break; case BE_OBS_VM_HEARTBEAT: { // AddLog(LOG_LEVEL_INFO, ">>>: Heartbeat now=%i timeout=%i", millis(), berry.timeout); if (berry.timeout) { if (TimeReached(berry.timeout)) { be_raise(vm, "timeout_error", "Berry code running for too long"); } } } break; case BE_OBS_MALLOC_FAIL: { int32_t vm_usage2 = va_arg(param, int32_t); AddLog(LOG_LEVEL_ERROR, D_LOG_BERRY "*** MEMORY ALLOCATION FAILED *** usage %i bytes", vm_usage2); } break; default: break; } va_end(param); } /*********************************************************************************************\ * Adde Berry metrics to teleperiod \*********************************************************************************************/ void BrShowState(void); void BrShowState(void) { if (berry.vm) { // trigger a gc first be_gc_collect(berry.vm); ResponseAppend_P(PSTR(",\"Berry\":{\"HeapUsed\":%u,\"Objects\":%u}"), berry.vm->gc.usage / 1024, berry.vm->counter_gc_kept); } } /*********************************************************************************************\ * VM Init \*********************************************************************************************/ extern "C" void be_webserver_cb_deinit(bvm *vm); void BerryInit(void) { // clean previous VM if any if (berry.vm != nullptr) { be_cb_deinit(berry.vm); // deregister any C callback for this VM #ifdef USE_WEBSERVER be_webserver_cb_deinit(berry.vm); // deregister C callbacks managed by webserver #endif // USE_WEBSERVER be_vm_delete(berry.vm); berry.vm = nullptr; berry.web_add_handler_done = false; berry.autoexec_done = false; berry.repl_active = false; berry.rules_busy = false; berry.timeout = 0; } int32_t ret_code1, ret_code2; bool berry_init_ok = false; do { berry.vm = be_vm_new(); /* create a virtual machine instance */ be_set_obs_hook(berry.vm, &BerryObservability); /* attach observability hook */ be_set_obs_micros(berry.vm, (bmicrosfnct)µs); comp_set_named_gbl(berry.vm); /* Enable named globals in Berry compiler */ comp_set_strict(berry.vm); /* Enable strict mode in Berry compiler, equivalent of `import strict` */ be_set_ctype_func_hanlder(berry.vm, be_call_ctype_func); if (UsePSRAM()) { // if PSRAM is available, raise the max size to 512kb berry.vm->bytesmaxsize = 512 * 1024; } be_load_custom_libs(berry.vm); // load classes and modules // Set the GC threshold to 3584 bytes to avoid the first useless GC berry.vm->gc.threshold = 3584; ret_code1 = be_loadstring(berry.vm, berry_prog); if (ret_code1 != 0) { be_error_pop_all(berry.vm); // clear Berry stack break; } // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_BERRY "Berry code loaded, RAM used=%u"), be_gc_memcount(berry.vm)); ret_code2 = be_pcall(berry.vm, 0); if (ret_code1 != 0) { be_error_pop_all(berry.vm); // clear Berry stack break; } // AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_BERRY "Berry code ran, RAM used=%u"), be_gc_memcount(berry.vm)); if (be_top(berry.vm) > 1) { be_error_pop_all(berry.vm); // clear Berry stack } else { be_pop(berry.vm, 1); } AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_BERRY "Berry initialized, RAM used %u bytes"), callBerryGC()); berry_init_ok = true; // we generate a synthetic event `autoexec` callBerryEventDispatcher(PSTR("preinit"), nullptr, 0, nullptr); // Run pre-init BrLoad("preinit.be"); // run 'preinit.be' if present } while (0); if (!berry_init_ok) { // free resources if (berry.vm != nullptr) { be_vm_delete(berry.vm); berry.vm = nullptr; } } } /*********************************************************************************************\ * BrRestart - restart a fresh new Berry vm, unloading everything from previous VM \*********************************************************************************************/ void CmndBrRestart(void) { if (berry.vm == nullptr) { ResponseCmndChar_P("Berry VM not started"); } BerryInit(); ResponseCmndChar_P("Berry VM restarted"); } /*********************************************************************************************\ * Execute a script in Flash file-system * * Two options supported: * berry_preinit: load "preinit.be" to configure the device before driver pre-init and init * (typically I2C drivers, and AXP192/AXP202 configuration) * berry_autoexec: load "autoexec.be" once all drivers are initialized \*********************************************************************************************/ void BrLoad(const char * script_name) { if (berry.vm == nullptr || TasmotaGlobal.no_autoexec) { return; } // abort is berry is not running, or bootloop prevention kicked in if (!strcmp_P(script_name, "autoexec.be")) { if (Settings->flag6.berry_no_autoexec) { // SetOption153 - (Berry) Disable autoexec.be on restart (1) return; } } be_getglobal(berry.vm, PSTR("load")); if (!be_isnil(berry.vm, -1)) { be_pushstring(berry.vm, script_name); BrTimeoutStart(); if (be_pcall(berry.vm, 1) != 0) { be_error_pop_all(berry.vm); // clear Berry stack return; } BrTimeoutReset(); bool loaded = be_tobool(berry.vm, -2); // did it succeed? be_pop(berry.vm, 2); if (loaded) { AddLog(LOG_LEVEL_INFO, D_LOG_BERRY "Successfully loaded '%s'", script_name); } else { AddLog(LOG_LEVEL_DEBUG, D_LOG_BERRY "No '%s'", script_name); } } } /*********************************************************************************************\ * Tasmota Commands \*********************************************************************************************/ // // Command `BrRun` // void CmndBrRun(void) { int32_t ret_code; const char * ret_val; if (berry.vm == nullptr) { ResponseCmndChar_P(PSTR(D_BR_NOT_STARTED)); return; } char br_cmd[XdrvMailbox.data_len+12]; // encapsulate into a function, copied from `be_repl.c` / `try_return()` snprintf_P(br_cmd, sizeof(br_cmd), PSTR("return (%s)"), XdrvMailbox.data); checkBeTop(); do { // First try with the `return ()` wrapper ret_code = be_loadbuffer(berry.vm, PSTR("input"), br_cmd, strlen(br_cmd)); if (be_getexcept(berry.vm, ret_code) == BE_SYNTAX_ERROR) { be_pop(berry.vm, 2); // remove exception values // if fails, try the direct command ret_code = be_loadbuffer(berry.vm, PSTR("input"), XdrvMailbox.data, strlen(XdrvMailbox.data)); } if (0 != ret_code) break; BrTimeoutStart(); ret_code = be_pcall(berry.vm, 0); // execute code BrTimeoutReset(); } while (0); if (0 == ret_code) { // code taken from REPL, look first at top, and if nil, look at return value // if (!be_isnil(berry.vm, 1)) { ret_val = be_tostring(berry.vm, 1); // } else { // ret_val = be_tostring(berry.vm, 2); // } Response_P("{\"" D_PRFX_BR "\":\"%s\"}", EscapeJSONString(ret_val).c_str()); // can't use XdrvMailbox.command as it may have been overwritten by subcommand be_pop(berry.vm, 1); } else { Response_P(PSTR("{\"" D_PRFX_BR "\":\"[%s] %s\"}"), EscapeJSONString(be_tostring(berry.vm, -2)).c_str(), EscapeJSONString(be_tostring(berry.vm, -1)).c_str()); be_pop(berry.vm, 2); } checkBeTop(); } /*********************************************************************************************\ * Berry console \*********************************************************************************************/ #ifdef USE_WEBSERVER void BrREPLRun(char * cmd) { if (berry.vm == nullptr) { return; } size_t cmd_len = strlen(cmd); size_t cmd2_len = cmd_len + 12; char * cmd2 = (char*) malloc(cmd2_len); do { int32_t ret_code; snprintf_P(cmd2, cmd2_len, PSTR("return (%s)"), cmd); ret_code = be_loadbuffer(berry.vm, PSTR("input"), cmd2, strlen(cmd2)); // AddLog(LOG_LEVEL_INFO, PSTR(">>>> be_loadbuffer cmd2 '%s', ret=%i"), cmd2, ret_code); if (be_getexcept(berry.vm, ret_code) == BE_SYNTAX_ERROR) { be_pop(berry.vm, 2); // remove exception values // if fails, try the direct command ret_code = be_loadbuffer(berry.vm, PSTR("input"), cmd, cmd_len); // AddLog(LOG_LEVEL_INFO, PSTR(">>>> be_loadbuffer cmd1 '%s', ret=%i"), cmd, ret_code); } if (0 == ret_code) { // code is ready to run BrTimeoutStart(); ret_code = be_pcall(berry.vm, 0); // execute code BrTimeoutReset(); // AddLog(LOG_LEVEL_INFO, PSTR(">>>> be_pcall ret=%i"), ret_code); if (0 == ret_code) { if (!be_isnil(berry.vm, 1)) { const char * ret_val = be_tostring(berry.vm, 1); berry.log.addString(ret_val, nullptr, "\n"); // AddLog(LOG_LEVEL_INFO, PSTR(">>> %s"), ret_val); } be_pop(berry.vm, 1); } } if (BE_EXCEPTION == ret_code) { be_error_pop_all(berry.vm); // clear Berry stack } } while(0); if (cmd2 != nullptr) { free(cmd2); cmd2 = nullptr; } checkBeTop(); } const char HTTP_SCRIPT_BERRY_CONSOLE[] PROGMEM = "var sn=0,id=0,ft,ltm=%d;" // Scroll position, Get most of weblog initially // Console command history "var hc=[],cn=0;" // hc = History commands, cn = Number of history being shown "function l(p){" // Console log and command service "var c,cc,o='';" "clearTimeout(lt);" "clearTimeout(ft);" "t=eb('t1');" "if(p==1){" "c=eb('c1');" // Console command id "cc=c.value.trim();" "if(cc){" "o='&c1='+encodeURIComponent(cc);" "hc.length>19&&hc.pop();" "hc.unshift(cc);" "cn=0;" "}" "c.value='';" "t.scrollTop=1e8;" "sn=t.scrollTop;" "}" "if(t.scrollTop>=sn){" // User scrolled back so no updates "if(x!=null){x.abort();}" // Abort if no response within 2 seconds (happens on restart 1) "x=new XMLHttpRequest();" "x.onreadystatechange=()=>{" "if(x.readyState==4&&x.status==200){" "var d,t1;" "d=x.responseText.split(/" BERRY_CONSOLE_CMD_DELIMITER "/,2);" // Field separator "var d1=d.length>1?d[0]:null;" "if(d1){" "t1=document.createElement('div');" "t1.classList.add('br1');" "t1.innerText=d1;" "t.appendChild(t1);" "}" "d1=d.length>1?d[1]:d[0];" "if(d1){" "t1=document.createElement('div');" "t1.classList.add('br2');" "t1.innerText=d1;" "t.appendChild(t1);" "}" "t.scrollTop=1e8;" "sn=t.scrollTop;" "clearTimeout(ft);" "lt=setTimeout(l,ltm);" // webrefresh timer.... "}" "};" "x.open('GET','bc?c2='+id+o,true);" // Related to Webserver->hasArg("c2") and WebGetArg("c2", stmp, sizeof(stmp)) "x.send();" "ft=setTimeout(l,2e4);" // fail timeout, triggered 20s after asking for XHR "}else{" "lt=setTimeout(l,ltm);" // webrefresh timer.... "}" "c1.focus();" "return false;" "}" "wl(l);" // Load initial console text ; // Add console command key eventlistener after name has been synced with id (= wl(jd)) const char HTTP_SCRIPT_BERRY_CONSOLE2[] PROGMEM = // // Console command history // "var hc=[],cn=0;" // hc = History commands, cn = Number of history being shown "var pc=0;" // pc = previous char "function h(){" // "if(!(navigator.maxTouchPoints||'ontouchstart'in document.documentElement)){eb('c1').autocomplete='off';}" // No touch so stop browser autocomplete "eb('c1').addEventListener('keydown',function(e){" "var b=eb('c1'),c=e.keyCode;" // c1 = Console command id "if((38==c||40==c)&&0==this.selectionStart&&0==this.selectionEnd){" "b.autocomplete='off';" "e.preventDefault();" "38==c?(++cn>hc.length&&(cn=hc.length),b.value=hc[cn-1]||''):" // ArrowUp "40==c?(0>--cn&&(cn=0),b.value=hc[cn-1]||''):" // ArrowDown "0;" "this.selectionStart=this.selectionEnd=0;" "}" // ArrowUp or ArrowDown must be a keyboard so stop browser autocomplete "if(c==13&&pc==13){" "e.preventDefault();" // prevent 'enter' from being inserted "l(1);" "}" "if(c==9){" "e.preventDefault();" "var start=this.selectionStart;" "var end=this.selectionEnd;" // set textarea value to: text before caret + tab + text after caret "this.value=this.value.substring(0, start)+\" \"+this.value.substring(end);" // put caret at right position again "this.selectionStart=this.selectionEnd=start + 1;" "}" "pc=c;" // record previous key // "13==c&&(hc.length>19&&hc.pop(),hc.unshift(b.value),cn=0)" // Enter, 19 = Max number -1 of commands in history "});" "}" "wl(h);"; // Add console command key eventlistener after name has been synced with id (= wl(jd)) const char HTTP_BERRY_STYLE_CMND[] PROGMEM = "" ; const char HTTP_BERRY_FORM_CMND[] PROGMEM = "
" "
" "
Welcome to the Berry Scripting console. " "Check the documentation." "
" "
" "
" "" "" "
" #ifdef USE_BERRY_DEBUG "

" "" "

" #endif // USE_BERRY_DEBUG ; const char HTTP_BTN_BERRY_CONSOLE[] PROGMEM = "

"; void HandleBerryConsoleRefresh(void) { String svalue = Webserver->arg(F("c1")); svalue.trim(); if (svalue.length()) { berry.log.reset(); // clear all previous logs berry.repl_active = true; // start recording // AddLog(LOG_LEVEL_INFO, PSTR("BRY: received command %s"), svalue.c_str()); berry.log.addString(svalue.c_str(), nullptr, BERRY_CONSOLE_CMD_DELIMITER); // Call berry BrREPLRun((char*)svalue.c_str()); berry.repl_active = false; // don't record further } WSContentBegin(200, CT_PLAIN); if (!berry.log.isEmpty()) { WSContentFlush(); for (auto & l: berry.log.log) { _WSContentSend(l.getBuffer()); } berry.log.reset(); } WSContentEnd(); } void HandleBerryConsole(void) { if (!HttpCheckPriviledgedAccess()) { return; } if (Webserver->hasArg(F("c2"))) { // Console refresh requested HandleBerryConsoleRefresh(); return; } if (Webserver->hasArg(F("rst"))) { // restart VM BerryInit(); Webserver->sendHeader("Location", "/bc", true); Webserver->send(302, "text/plain", ""); } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP "Berry " D_CONSOLE)); WSContentStart_P(PSTR("Berry " D_CONSOLE)); WSContentSend_P(HTTP_SCRIPT_BERRY_CONSOLE, Settings->web_refresh); WSContentSend_P(HTTP_SCRIPT_BERRY_CONSOLE2); WSContentSendStyle(); WSContentFlush(); _WSContentSend(HTTP_BERRY_STYLE_CMND); _WSContentSend(HTTP_BERRY_FORM_CMND); WSContentSpaceButton(BUTTON_MANAGEMENT); WSContentStop(); } // const BeBECCode_t BECCode[] = { // struct BeBECCode_t { // const char * display_name; // display name in Web UI (must be URL encoded) // const char * id; // id in requested URL // const char * url; // absolute URL to download the bec file // const char * redirect; // relative URI to redirect after loading // }; // Display Buttons to dynamically load bec files void HandleBerryBECLoaderButton(void) { bvm * vm = berry.vm; if (vm == NULL) { return; } // Berry vm is not initialized for (int32_t i = 0; i < ARRAY_SIZE(BECCode); i++) { const BeBECCode_t &bec = BECCode[i]; if (!(*bec.loaded)) { if (be_global_find(vm, be_newstr(vm, bec.id)) < 0) { // the global name doesn't exist WSContentSend_P("

", bec.id, bec.display_name); } else { *bec.loaded = true; } } } } extern "C" bbool BerryBECLoader(const char * url); void HandleBerryBECLoader(void) { String n = Webserver->arg("n"); for (int32_t i = 0; i < ARRAY_SIZE(BECCode); i++) { const BeBECCode_t &bec = BECCode[i]; if (n.equals(bec.id)) { if (BerryBECLoader(bec.url)) { // All good, redirect Webserver->sendHeader("Location", bec.redirect, true); Webserver->send(302, "text/plain", ""); *bec.loaded = true; } else { Webserver->sendHeader("Location", "/mn?", true); Webserver->send(302, "text/plain", ""); } } } } // return true if successful extern "C" bbool BerryBECLoader(const char * url) { bvm *vm = berry.vm; HTTPClientLight cl; cl.setUserAgent(USE_BERRY_WEBCLIENT_USERAGENT); cl.setConnectTimeout(USE_BERRY_WEBCLIENT_TIMEOUT); // set default timeout cl.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); if (!cl.begin(url)) { AddLog(LOG_LEVEL_INFO, "BRY: unable to load URL '%s'", url); // cl.end(); return false; } uint32_t http_connect_time = millis(); int32_t httpCode = cl.GET(); if (httpCode != 200) { AddLog(LOG_LEVEL_INFO, "BRY: unable to load URL '%s' code %i", url, httpCode); // cl.end(); return false; } int32_t sz = cl.getSize(); AddLog(LOG_LEVEL_DEBUG, "BRY: Response http_code %i size %i bytes in %i ms", httpCode, sz, millis() - http_connect_time); // abort if we exceed 32KB size, things will not go well otherwise if (sz >= 32767 || sz <= 0) { AddLog(LOG_LEVEL_DEBUG, "BRY: Response size too big %i bytes", sz); return false; } // create a bytes object at top of stack. // the streamwriter knows how to get it. uint8_t * buf = (uint8_t*) be_pushbytes(vm, nullptr, sz); StreamBeBytesWriter memory_writer(vm); int32_t written = cl.writeToStream(&memory_writer); cl.end(); // free allocated memory ~16KB size_t loaded_sz = 0; const void * loaded_buf = be_tobytes(vm, -1, &loaded_sz); FlashFileImplPtr fp = FlashFileImplPtr(new FlashFileImpl(loaded_buf, loaded_sz)); File * f_ptr = new File(fp); // we need to allocate dynamically because be_close calls `delete` on it bclosure* loaded_bec = be_bytecode_load_from_fs(vm, f_ptr); be_pop(vm, 1); if (loaded_bec != NULL) { be_pushclosure(vm, loaded_bec); be_call(vm, 0); be_pop(vm, 1); } be_gc_collect(vm); // force a GC to free the buffer now return true; } #endif // USE_WEBSERVER /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv52(uint32_t function) { bool result = false; switch (function) { case FUNC_SLEEP_LOOP: if (TasmotaGlobal.berry_fast_loop_enabled) { // call only if enabled at global level callBerryFastLoop(true); // call `tasmota.fast_loop()` optimized for minimal performance impact } break; case FUNC_LOOP: if (!berry.autoexec_done) { // we generate a synthetic event `autoexec` callBerryEventDispatcher(PSTR("autoexec"), nullptr, 0, nullptr); BrLoad("autoexec.be"); // run autoexec.be at first tick, so we know all modules are initialized berry.autoexec_done = true; #ifdef USE_WEBSERVER // check if `web_add_handler` was missed, for example because of Berry VM restart if (!berry.web_add_handler_done) { bool network_up = WifiHasIP(); #ifdef USE_ETHERNET network_up = network_up || EthernetHasIP(); #endif if (network_up && (Webserver != NULL)) { // if network is already up, send a synthetic event to trigger web handlers callBerryEventDispatcher(PSTR("web_add_handler"), nullptr, 0, nullptr); berry.web_add_handler_done = true; } } #endif // USE_WEBSERVER } if (TasmotaGlobal.berry_fast_loop_enabled) { // call only if enabled at global level callBerryFastLoop(false); // call `tasmota.fast_loop()` optimized for minimal performance impact } break; // Berry wide commands and events case FUNC_RULES_PROCESS: result = callBerryRule(nullptr, false); break; case FUNC_TELEPERIOD_RULES_PROCESS: result = callBerryRule(nullptr, true); break; case FUNC_MQTT_DATA: result = callBerryEventDispatcher(PSTR("mqtt_data"), XdrvMailbox.topic, 0, XdrvMailbox.data, XdrvMailbox.data_len); break; case FUNC_COMMAND: result = DecodeCommand(kBrCommands, BerryCommand); if (!result) { result = callBerryEventDispatcher(PSTR("cmd"), XdrvMailbox.topic, XdrvMailbox.index, XdrvMailbox.data); } break; // Module specific events case FUNC_EVERY_50_MSECOND: callBerryEventDispatcher(PSTR("every_50ms"), nullptr, 0, nullptr); break; case FUNC_EVERY_100_MSECOND: callBerryEventDispatcher(PSTR("every_100ms"), nullptr, 0, nullptr); break; case FUNC_EVERY_250_MSECOND: callBerryEventDispatcher(PSTR("every_250ms"), nullptr, 0, nullptr); break; case FUNC_EVERY_SECOND: callBerryEventDispatcher(PSTR("every_second"), nullptr, 0, nullptr); break; case FUNC_SET_DEVICE_POWER: result = callBerryEventDispatcher(PSTR("set_power_handler"), nullptr, XdrvMailbox.index, nullptr); break; case FUNC_BUTTON_PRESSED: { static uint32_t timer_last_button_sent = 0; // XdrvMailbox.index = button_index; // XdrvMailbox.payload = button; // XdrvMailbox.command_code = Button.last_state[button_index]; uint8_t state = (XdrvMailbox.command_code & 0xFF); uint8_t multipress_state = (XdrvMailbox.command_code >> 8) & 0xFF; if ((XdrvMailbox.payload != state) || TimeReached(timer_last_button_sent)) { // fire event only when state changes timer_last_button_sent = millis() + 1000; // wait for 1 second result = callBerryEventDispatcher(PSTR("button_pressed"), nullptr, (multipress_state & 0xFF) << 24 | (XdrvMailbox.payload & 0xFF) << 16 | (XdrvMailbox.command_code & 0xFF) << 8 | (XdrvMailbox.index & 0xFF) , nullptr); } } break; case FUNC_BUTTON_MULTI_PRESSED: // XdrvMailbox.index = button_index; // XdrvMailbox.payload = Button.press_counter[button_index]; result = callBerryEventDispatcher(PSTR("button_multi_pressed"), nullptr, (XdrvMailbox.payload & 0xFF) << 8 | (XdrvMailbox.index & 0xFF) , nullptr); break; case FUNC_ANY_KEY: // XdrvMailbox.payload = device_save << 24 | key << 16 | state << 8 | device; // key 0 = KEY_BUTTON = button_topic // key 1 = KEY_SWITCH = switch_topic // state 0 = POWER_OFF = off // state 1 = POWER_ON = on // state 2 = POWER_TOGGLE = toggle // state 3 = POWER_HOLD = hold // state 4 = POWER_INCREMENT = button still pressed // state 5 = POWER_INV = button released // state 6 = POWER_CLEAR = button released // state 7 = POWER_RELEASE = button released // state 9 = CLEAR_RETAIN = clear retain flag // state 10 = POWER_DELAYED = button released delayed // Button Multipress // state 10 = SINGLE // state 11 = DOUBLE // state 12 = TRIPLE // state 13 = QUAD // state 14 = PENTA result = callBerryEventDispatcher("any_key", nullptr, XdrvMailbox.payload, nullptr); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_CONSOLE_BUTTON: if (XdrvMailbox.index) { XdrvMailbox.index++; } else if (berry.vm != NULL) { WSContentSend_P(HTTP_BTN_BERRY_CONSOLE); HandleBerryBECLoaderButton(); // display buttons to load BEC files callBerryEventDispatcher(PSTR("web_add_button"), nullptr, 0, nullptr); callBerryEventDispatcher(PSTR("web_add_console_button"), nullptr, 0, nullptr); } break; case FUNC_WEB_ADD_MAIN_BUTTON: callBerryEventDispatcher(PSTR("web_add_main_button"), nullptr, 0, nullptr); break; case FUNC_WEB_ADD_MANAGEMENT_BUTTON: callBerryEventDispatcher(PSTR("web_add_management_button"), nullptr, 0, nullptr); break; case FUNC_WEB_ADD_BUTTON: callBerryEventDispatcher(PSTR("web_add_config_button"), nullptr, 0, nullptr); break; case FUNC_WEB_ADD_HANDLER: if (!berry.web_add_handler_done) { callBerryEventDispatcher(PSTR("web_add_handler"), nullptr, 0, nullptr); berry.web_add_handler_done = true; } WebServer_on("/bc", HandleBerryConsole); WebServer_on("/tapp", HandleBerryBECLoader, HTTP_GET); break; #endif // USE_WEBSERVER case FUNC_SAVE_BEFORE_RESTART: callBerryEventDispatcher(PSTR("save_before_restart"), nullptr, 0, nullptr); break; case FUNC_WEB_SENSOR: callBerryEventDispatcher(PSTR("web_sensor"), nullptr, 0, nullptr); break; case FUNC_WEB_GET_ARG: callBerryEventDispatcher(PSTR("web_get_arg"), nullptr, 0, nullptr); break; case FUNC_JSON_APPEND: callBerryEventDispatcher(PSTR("json_append"), nullptr, 0, nullptr); break; case FUNC_AFTER_TELEPERIOD: callBerryEventDispatcher(PSTR("after_teleperiod"), nullptr, 0, nullptr); break; case FUNC_ACTIVE: result = true; break; } return result; } #endif // USE_BERRY