Tasmota/tasmota/tasmota_xdrv_driver/xdrv_52_9_berry.ino

1049 lines
37 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
#ifdef USE_BERRY
#define XDRV_52 52
#include <berry.h>
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)&micros);
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 =
"<style>"
".br1{" // berry output
"border-left:dotted 2px #860;"
"margin-bottom:4px;"
"margin-top:4px;"
"padding:1px 5px 1px 18px;"
"}"
".br2{" // user input
"padding:0px 5px 0px 5px;"
"color:#faffff;"
"}"
".br0{"
// "-moz-appearance: textfield-multiline;"
// "-webkit-appearance: textarea;"
"font:medium -moz-fixed;"
"font:-webkit-small-control;"
"box-sizing:border-box;"
"width:100%;"
"overflow:auto;"
"resize:vertical;"
"font-family:monospace;"
"overflow:auto;"
"font-size:1em;"
"}"
".bro{"
// "-moz-appearance: textfield-multiline;"
// "-webkit-appearance: textarea;"
"border:1px solid gray;"
"height:250px;"
"padding:2px;"
"background:#222;"
"color:#fb1;"
"white-space:pre;"
"padding:2px 5px 2px 5px;"
"}"
".bri{"
// "-moz-appearance: textfield-multiline;"
// "-webkit-appearance: textarea;"
"border:1px solid gray;"
"height:60px;"
"padding:5px;"
"color:#000000;background:#faffff"
"}"
"</style>"
;
const char HTTP_BERRY_FORM_CMND[] PROGMEM =
"<br>"
"<div contenteditable='false' class='br0 bro' readonly id='t1' cols='340' wrap='off'>"
"<div class='br1'>Welcome to the Berry Scripting console. "
"Check the <a href='https://tasmota.github.io/docs/Berry/' target='_blank'>documentation</a>."
"</div>"
"</div>"
"<form method='get' id='fo' onsubmit='return l(1);'>"
"<textarea id='c1' class='br0 bri' rows='4' cols='340' wrap='soft' autofocus required></textarea>"
"<button type='submit'>Run code (or press 'Enter' twice)</button>"
"</form>"
#ifdef USE_BERRY_DEBUG
"<p><form method='post' >"
"<button type='submit' name='rst' class='bred' onclick=\"if(confirm('Confirm removing endpoint')){clearTimeout(lt);return true;}else{return false;}\">Restart Berry VM (for devs only)</button>"
"</form></p>"
#endif // USE_BERRY_DEBUG
;
const char HTTP_BTN_BERRY_CONSOLE[] PROGMEM =
"<p><form action='bc' method='get'><button>Berry Scripting console</button></form></p>";
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("<form id=but_part_mgr style='display: block;' action='tapp' method='get'><input type='hidden' name='n' value='%s'/><button>[Load %s]</button></form><p></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