diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp.be b/lib/libesp32/berry_tasmota/src/embedded/openhasp.be deleted file mode 100644 index 4232a605b..000000000 --- a/lib/libesp32/berry_tasmota/src/embedded/openhasp.be +++ /dev/null @@ -1,764 +0,0 @@ -import string -import json - -# lv.start() -# scr = lv.scr_act() # default screean object -# scr.set_style_bg_color(lv.color(0x0000A0), lv.PART_MAIN | lv.STATE_DEFAULT) - -lv.start() - -hres = lv.get_hor_res() # should be 320 -vres = lv.get_ver_res() # should be 240 - -scr = lv.scr_act() # default screean object -#f20 = lv.montserrat_font(20) # load embedded Montserrat 20 -r20 = lv.font_robotocondensed_latin1(20) -r16 = lv.font_robotocondensed_latin1(16) - -th2 = lv.theme_openhasp_init(0, lv.color(0xFF00FF), lv.color(0x303030), false, r16) -scr.get_disp().set_theme(th2) -# TODO -scr.set_style_bg_color(lv.color(lv.COLOR_WHITE),0) - -# apply theme to layer_top, but keep it transparent -lv.theme_apply(lv.layer_top()) -lv.layer_top().set_style_bg_opa(0,0) - - -# takes an attribute name and responds if it needs color conversion -def is_color_attribute(t) - import string - t = str(t) - # contains `color` but does not contain `color_` - return (string.find(t, "color") >= 0) && (string.find(t, "color_") < 0) -end - -# parse hex string -def parse_hex(s) - import string - s = string.toupper(s) # turn to uppercase - var val = 0 - for i:0..size(s)-1 - var c = s[i] - # var c_int = string.byte(c) - if c == "#" continue end # skip '#' prefix if any - if c == "x" || c == "X" continue end # skip 'x' or 'X' - - if c >= "A" && c <= "F" - val = (val << 4) | string.byte(c) - 55 - elif c >= "0" && c <= "9" - val = (val << 4) | string.byte(c) - 48 - end - end - return val -end - -def parse_color(s) - s = str(s) - if s[0] == '#' - return lv.color(parse_hex(s)) - else - import string - import introspect - var col_name = "COLOR_" + string.toupper(s) - var col_try = introspect.get(lv, col_name) - if col_try != nil - return lv.color(col_try) - end - end - # fail safe with black color - return lv.color(0x000000) -end - -#- ------------------------------------------------------------ - Class `lvh_obj` encapsulating `lv_obj`` - - Provide a mapping for virtual members - Stores the associated page and object id - - Adds specific virtual members used by OpenHASP -- ------------------------------------------------------------ -# -class lvh_obj - # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses - static _lv_class = lv.obj - static _lv_part2_selector # selector for secondary part (like knob of arc) - - # attributes to ignore when set at object level (they are managed by page) - static _attr_ignore = [ - "id", - "obj", - "page", - "comment", - "parentid", - "auto_size", # TODO not sure it's still needed in LVGL8 - ] - #- mapping from OpenHASP attribute to LVGL attribute -# - #- if mapping is null, we use set_X and get_X from our own class -# - static _attr_map = { - "x": "x", - "y": "y", - "w": "width", - "h": "height", - # arc - "asjustable": nil, - "mode": nil, - "start_angle": "bg_start_angle", - "start_angle1": "start_angle", - "end_angle": "bg_end_angle", - "end_angle1": "end_angle", - "radius": "style_radius", - "border_side": "style_border_side", - "bg_opa": "style_bg_opa", - "border_width": "style_border_width", - "line_width": nil, # depebds on class - "line_width1": nil, # depebds on class - "action": nil, # store the action in self._action - "hidden": nil, # apply to self - "enabled": nil, # apply to self - "click": nil, # synonym to enabled - "toggle": nil, - "bg_color": "style_bg_color", - "bg_grad_color": "style_bg_grad_color", - "type": nil, - # below automatically create a sub-label - "text": nil, # apply to self - "value_str": nil, # synonym to 'text' - "align": nil, - "text_font": nil, - "value_font": nil, # synonym to text_font - "text_color": nil, - "value_color": nil, # synonym to text_color - "value_ofs_x": nil, - "value_ofs_y": nil, - # - "min": nil, - "max": nil, - "val": "value", - "rotation": "rotation", - # img - "src": "src", - "image_recolor": "style_img_recolor", - "image_recolor_opa": "style_img_recolor_opa", - # spinner - "angle": nil, - "speed": nil, - # padding of knob - "pad_top2": nil, - "pad_bottom2": nil, - "pad_left2": nil, - "pad_right2": nil, - "pad_all2": nil, - "radius2": nil, - } - - var _lv_obj # native lvgl object - var _lv_label # sub-label if exists - var _action # action for OpenHASP - - # init - # - create the LVGL encapsulated object - # arg1: parent object - # arg2: json line object - def init(parent, jline) - var obj_class = self._lv_class # need to assign to a var to distinguish from method call - self._lv_obj = obj_class(parent) # instanciate LVGL object - self.post_init() - end - - # post-init, to be overriden - def post_init() - end - - # get LVGL encapsulated object - def get_obj() - return self._lv_obj - end - - def set_action(t) - self._action = str(t) - end - def get_action() - return self._action() - end - - def set_line_width(t) - self._lv_obj.set_style_line_width(int(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_line_width() - return self._lv_obj.get_style_line_width(lv.PART_MAIN | lv.STATE_DEFAULT) - end - - #- ------------------------------------------------------------ - Mapping of synthetic attributes - - text - - hidden - - enabled - - ------------------------------------------------------------ -# - #- `hidden` attributes mapped to OBJ_FLAG_HIDDEN -# - def set_hidden(h) - if h - self._lv_obj.add_flag(lv.OBJ_FLAG_HIDDEN) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_HIDDEN) - end - end - - def get_hidden() - return self._lv_obj.has_flag(lv.OBJ_FLAG_HIDDEN) - end - - #- `enabled` attributes mapped to OBJ_FLAG_CLICKABLE -# - def set_enabled(h) - if h - self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) - end - end - - def get_enabled() - return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) - end - # click is synonym to enabled - def set_click(t) self.set_enabled(t) end - def get_click() return self.get_enabled() end - - #- `toggle` attributes mapped to STATE_CHECKED -# - def set_toggle(t) - if t == "TRUE" t = true end - if t == "FALSE" t = false end - if t - self._lv_obj.add_state(lv.STATE_CHECKED) - else - self._lv_obj.clear_state(lv.STATE_CHECKED) - end - end - - def get_toggle() - return self._lv_obj.has_state(lv.STATE_CHECKED) - end - - def set_adjustable(t) - if t - self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) - else - self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) - end - end - def get_adjustable() - return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) - end - - #- set_text: create a `lv_label` sub object to the current object -# - #- (default case, may be overriden by object that directly take text) -# - def check_label() - if self._lv_label == nil - self._lv_label = lv.label(self.get_obj()) - self._lv_label.set_align(lv.ALIGN_CENTER); - end - end - - def set_text(t) - self.check_label() - self._lv_label.set_text(str(t)) - end - def set_value_str(t) self.set_text(t) end - - def get_text() - if self._lv_label == nil return nil end - return self._lv_label.get_text() - end - def get_value_str() return self.get_text() end - - def set_align(t) - var align - self.check_label() - if t == 0 || t == "left" - align = lv.TEXT_ALIGN_LEFT - elif t == 1 || t == "center" - align = lv.TEXT_ALIGN_CENTER - elif t == 2 || t == "right" - align = lv.TEXT_ALIGN_RIGHT - end - self._lv_label.set_style_text_align(align, lv.PART_MAIN | lv.STATE_DEFAULT) - end - - def get_align() - if self._lv_label == nil return nil end - var align self._lv_label.get_style_text_align(lv.PART_MAIN | lv.STATE_DEFAULT) - if align == lv.TEXT_ALIGN_LEFT - return "left" - elif align == lv.TEXT_ALIGN_CENTER - return "center" - elif align == lv.TEXT_ALIGN_RIGHT - return "right" - else - return nil - end - end - - def set_text_font(t) - self.check_label() - var f = lv.font_robotocondensed_latin1(int(t)) - if f != nil - self._lv_label.set_style_text_font(f, lv.PART_MAIN | lv.STATE_DEFAULT) - else - print("HSP: Unsupported font size: robotocondensed-latin1", t) - end - end - def get_text_font() - end - def set_value_font(t) self.set_text_font(t) end - def get_value_font() return self.get_text_font() end - - def set_text_color(t) - self.check_label() - self._lv_label.set_style_text_color(parse_color(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_text_color() - return self._text_color - end - def set_value_color(t) self.set_text_color(t) end - def get_value_color() return self.get_value_color() end - - def set_value_ofs_x(t) - self.check_label() - self._lv_label.set_x(int(t)) - end - def get_value_ofs_x() - return self._lv_label.get_x() - end - def set_value_ofs_y(t) - self.check_label() - self._lv_label.set_y(int(t)) - end - def get_value_ofs_y() - return self._lv_label.get_y() - end - - # secondary element - def set_pad_top2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_top(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_bottom2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_bottom(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_left2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_left(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_right2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_right(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def set_pad_all2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_pad_all(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - - def get_pad_top() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_top(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_bottomo() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_bottom(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_left() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_left(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_right() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_pad_right(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_pad_all() - end - - def set_radius2(t) - if self._lv_part2_selector != nil - self._lv_obj.set_style_radius(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - def get_radius2() - if self._lv_part2_selector != nil - return self._lv_obj.get_style_radius(self._lv_part2_selector | lv.STATE_DEFAULT) - end - end - - #- ------------------------------------------------------------ - Mapping of virtual attributes - - ------------------------------------------------------------ -# - def member(k) - # tostring is a special case, we shouldn't raise an exception for it - if k == 'tostring' return nil end - # - if self._attr_map.has(k) - import introspect - var kv = self._attr_map[k] - if kv - var f = introspect.get(self._lv_obj, "get_" + kv) - if type(f) == 'function' - return f(self._lv_obj) - end - else - # call self method - var f = introspect.get(self, "get_" + k) - if type(f) == 'function' - return f(self, k) - end - end - end - raise "value_error", "unknown attribute " + str(k) - end - - def setmember(k, v) - import string - # print(">> setmember", k, v) - # print(">>", classname(self), self._attr_map) - if self._attr_ignore.find(k) != nil - return - elif self._attr_map.has(k) - import introspect - var kv = self._attr_map[k] - if kv - var f = introspect.get(self._lv_obj, "set_" + kv) - # if the attribute contains 'color', convert to lv_color - if type(kv) == 'string' && is_color_attribute(kv) - v = parse_color(v) - end - # print("f=", f, v, kv, self._lv_obj, self) - if type(f) == 'function' - if string.find(kv, "style_") == 0 - # style function need a selector as second parameter - f(self._lv_obj, v, lv.PART_MAIN | lv.STATE_DEFAULT) - else - f(self._lv_obj, v) - end - return - else - print("HSP: Could not find function set_"+kv) - end - else - # call self method - var f = introspect.get(self, "set_" + k) - # print("f==",f) - if type(f) == 'function' - f(self, v) - return - end - end - - else - print("HSP: unknown attribute:", k) - end - # silently ignore if the attribute name is not supported - end -end - -#- ------------------------------------------------------------ - Other widgets -- ------------------------------------------------------------ -# - -#- ------------------------------------------------------------ - label -#- ------------------------------------------------------------# -class lvh_label : lvh_obj - static _lv_class = lv.label - # label do not need a sub-label - def post_init() - self._lv_label = self._lv_obj - end -end - -#- ------------------------------------------------------------ - arc -#- ------------------------------------------------------------# -class lvh_arc : lvh_obj - static _lv_class = lv.arc - static _lv_part2_selector = lv.PART_KNOB - - # line_width converts to arc_width - def set_line_width(t) - self._lv_obj.set_style_arc_width(int(t), lv.PART_MAIN | lv.STATE_DEFAULT) - end - def get_line_width() - return self._lv_obj.get_arc_line_width(lv.PART_MAIN | lv.STATE_DEFAULT) - end - def set_line_width1(t) - self._lv_obj.set_style_arc_width(int(t), lv.PART_INDICATOR | lv.STATE_DEFAULT) - end - def get_line_width1() - return self._lv_obj.get_arc_line_width(lv.PART_INDICATOR | lv.STATE_DEFAULT) - end - - def set_min(t) - self._lv_obj.set_range(int(t), self.get_max()) - end - def set_max(t) - self._lv_obj.set_range(self.get_min(), int(t)) - end - def get_min() - return self._lv_obj.get_min_value() - end - def get_max() - return self._lv_obj.get_max_value() - end - def set_type(t) - var mode - if t == 0 mode = lv.ARC_MODE_NORMAL - elif t == 1 mode = lv.ARC_MODE_REVERSE - elif t == 2 mode = lv.ARC_MODE_SYMMETRICAL - end - if mode != nil - self._lv_obj.set_mode(mode) - end - end - def get_type() - return self._lv_obj.get_mode() - end - # mode - def set_mode(t) - var mode - if mode == "expand" self._lv_obj.set_width(lv.SIZE_CONTENT) - elif mode == "break" mode = lv.LABEL_LONG_WRAP - elif mode == "dots" mode = lv.LABEL_LONG_DOT - elif mode == "scroll" mode = lv.LABEL_LONG_SCROLL - elif mode == "loop" mode = lv.LABEL_LONG_SCROLL_CIRCULAR - elif mode == "crop" mode = lv.LABEL_LONG_CLIP - end - if mode != nil - self._lv_obj.lv_label_set_long_mode(mode) - end - end - def get_mode() - end - -end - -#- ------------------------------------------------------------ - switch -#- ------------------------------------------------------------# -class lvh_switch : lvh_obj - static _lv_class = lv.switch - static _lv_part2_selector = lv.PART_KNOB -end - -#- ------------------------------------------------------------ - spinner -#- ------------------------------------------------------------# -class lvh_spinner : lvh_arc - static _lv_class = lv.spinner - - # init - # - create the LVGL encapsulated object - # arg1: parent object - # arg2: json line object - def init(parent, jline) - var angle = jline.find("angle", 60) - var speed = jline.find("speed", 1000) - self._lv_obj = lv.spinner(parent, speed, angle) - self.post_init() - end - - # ignore attributes, spinner can't be changed once created - def set_angle(t) end - def get_angle() end - def set_speed(t) end - def get_speed() end -end - -#- creat sub-classes of lvh_obj and map the LVGL class in static '_lv_class' attribute -# -class lvh_bar : lvh_obj static _lv_class = lv.bar end -class lvh_btn : lvh_obj static _lv_class = lv.btn end -class lvh_btnmatrix : lvh_obj static _lv_class = lv.btnmatrix end -class lvh_checkbox : lvh_obj static _lv_class = lv.checkbox end -class lvh_dropdown : lvh_obj static _lv_class = lv.dropdown end -class lvh_img : lvh_obj static _lv_class = lv.img end -class lvh_line : lvh_obj static _lv_class = lv.line end -class lvh_roller : lvh_obj static _lv_class = lv.roller end -class lvh_slider : lvh_obj static _lv_class = lv.slider end -class lvh_textarea : lvh_obj static _lv_class = lv.textarea end - -#- ---------------------------------------------------------------------------- - Class `lvh_page` encapsulating `lv_obj` as screen (created with lv.obj(0)) -- ----------------------------------------------------------------------------- -# -# ex of transition: lv.scr_load_anim(scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false) -class lvh_page - var _obj_id # (map) of objects by id numbers - var _page_id # (int) id number of the page - var _lv_scr # (lv_obj) lvgl screen object - - #- init(page_number) -# - def init(page_number) - import global - - # if no parameter, default to page #1 - if page_number == nil page_number = 1 end - - self._page_id = page_number # remember our page_number - self._obj_id = {} # init list of objects - if page_number == 1 - self._lv_scr = lv.scr_act() # default screen - elif page_number == 0 - self._lv_scr = lv.layer_top() # top layer, visible over all screens - else - self._lv_scr = lv.obj(0) # allocate a new screen - # self._lv_scr.set_style_bg_color(lv.color(0x000000), lv.PART_MAIN | lv.STATE_DEFAULT) # set black background - self._lv_scr.set_style_bg_color(lv.color(0xFFFFFF), lv.PART_MAIN | lv.STATE_DEFAULT) # set white background - end - - # create a global for this page of form p, ex p1 - var glob_name = string.format("p%i", self._page_id) - global.(glob_name) = self - end - - #- retrieve lvgl screen object for this page -# - def get_scr() - return self._lv_scr - end - - #- add an object to this page -# - def set_obj(id, o) - self._obj_id[id] = o - end - def get_obj(id) - return self._obj_id.find(id) - end - - #- return id of this page -# - def id() - return self._page_id - end - - #- show this page, with animation -# - def show(anim, duration) - # ignore if there is no screen, like for id 0 - if self._lv_scr == nil return nil end - # ignore if the screen is already active - if self._lv_scr._p == lv.scr_act()._p return end # do nothing - - # default animation is lv.SCR_LOAD_ANIM_MOVE_RIGHT - if anim == nil anim = lv.SCR_LOAD_ANIM_MOVE_RIGHT end - # default duration of 500ms - if duration == nil duration = 500 end - - # load new screen with anumation, no delay, 500ms transition time, no auto-delete - lv.scr_load_anim(self._lv_scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, duration, 0, false) - end -end - -#- pages -# -var lvh_page_cur = lvh_page(1) -var lvh_pages = { 1: lvh_page_cur } # always create page #1 - -f = open("pages.jsonl","r") -var jsonl = string.split(f.read(), "\n") -f.close() - -#- ------------------------------------------------------------ - Parse page information - - Create a new page object if required - Change the active page -- ------------------------------------------------------------ -# -def parse_page(jline) - if jline.has("page") && type(jline["page"]) == 'int' - var page = int(jline["page"]) - # does the page already exist? - if lvh_pages.has(page) - # yes, just change the current page - lvh_page_cur = lvh_pages[page] - else - # no, create a new page - lvh_page_cur = lvh_page(page) - lvh_pages[page] = lvh_page_cur - end - end -end - -#- ------------------------------------------------------------ - Parse single object - -- ------------------------------------------------------------ -# -def parse_obj(jline, page) - import global - import introspect - - # line must contain 'obj' and 'id', otherwise it is ignored - if jline.has("obj") && jline.has("id") && type(jline["id"]) == 'int' - # 'obj_id' must be between 1 and 254 - var obj_id = int(jline["id"]) - if obj_id < 1 || obj_id > 254 - raise "value error", "invalid id " + str(obj_id) - end - - # extract openhasp class, prefix with `lvh_`. Ex: `btn` becomes `lvh_btn` - var obj_type = jline["obj"] - - # extract parent - var parent - var parent_id = int(jline.find("parentid")) - if parent_id != nil - var parent_obj = lvh_page_cur.get_obj(parent_id) - if parent_obj != nil - parent = parent_obj._lv_obj - end - end - if parent == nil - parent = page.get_scr() - end - - # check if a class with the requested name exists - var obj_class = introspect.get(global, "lvh_" + obj_type) - if obj_class == nil - raise "value error", "cannot find object of type " + str(obj_type) - end - - # instanciate the object, passing the lvgl screen as paren object - var obj = obj_class(parent, jline) - - # add object to page object - lvh_page_cur.set_obj(obj_id, obj) - # set attributes - # try every attribute, if not supported it is silently ignored - for k:jline.keys() - # introspect.set(obj, k, jline[k]) - obj.(k) = jline[k] - end - - # create a global variable for this object of form pb, ex p1b2 - var glob_name = string.format("p%ib%i", lvh_page_cur.id(), obj_id) - global.(glob_name) = obj - end -end - -# ex: -# {'page': 1, 'h': 50, 'obj': 'label', 'hidden': false, 'text': 'Hello', 'x': 5, 'id': 1, 'enabled': true, 'y': 5, 'w': 50} -# {"page":1,"id":2,"obj":"btn","x":5,"y":90,"h":90,"w":50,"text":"World","enabled":false,"hidden":false} - -#- ------------------------------------------------------------ - Parse jsonl file line by line - -- ------------------------------------------------------------ -# -tasmota.yield() -for j:jsonl - var jline = json.load(j) - - # parse page first - if type(jline) == 'instance' - parse_page(jline) - parse_obj(jline, lvh_page_cur) - end -end diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be b/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be deleted file mode 100644 index 79462aa85..000000000 --- a/tasmota/berry/lvgl_examples/lv_tasmota_log_roboto.be +++ /dev/null @@ -1,117 +0,0 @@ -# lv_tasmota_log class - -class lv_tasmota_log_roboto : lv.obj - var label # contains the sub lv_label object - var lines - var line_len - var log_reader - var log_level - - def init(parent) - super(self).init(parent) - self.set_width(parent.get_width()) - self.set_pos(0, 0) - - self.set_style_bg_color(lv.color(0x000000), lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_bg_opa(255, lv.PART_MAIN | lv.STATE_DEFAULT) - self.move_background() - self.set_style_border_opa(255, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_radius(0, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_pad_all(2, lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_border_color(lv.color(0x0099EE), lv.PART_MAIN | lv.STATE_DEFAULT) - self.set_style_border_width(1, lv.PART_MAIN | lv.STATE_DEFAULT) - self.refr_size() - self.refr_pos() - - self.label = lv.label(self) - self.label.set_width(self.get_width() - 12) - - self.label.set_style_text_color(lv.color(0x00FF00), lv.PART_MAIN | lv.STATE_DEFAULT) - self.label.set_long_mode(lv.LABEL_LONG_CLIP) - var roboto12 = lv.font_robotocondensed_latin1(12) - self.label.set_style_text_font(roboto12, lv.PART_MAIN | lv.STATE_DEFAULT) - # var lg_font = lv.font_montserrat(10) - # self.set_style_text_font(lg_font, lv.PART_MAIN | lv.STATE_DEFAULT) - self.label.set_text("") # bug, still displays "Text" - - self.add_event_cb( / obj, evt -> self.size_changed_cb(obj, evt), lv.EVENT_SIZE_CHANGED | lv.EVENT_STYLE_CHANGED | lv.EVENT_DELETE, 0) - - self.lines = [] - self.line_len = 0 - self.log_reader = tasmota_log_reader() - self.log_level = 2 - self._size_changed() - - tasmota.add_driver(self) - end - - def set_lines_count(line_len) - if line_len > self.line_len # increase lines - for i: self.line_len .. line_len-1 - self.lines.insert(0, "") - end - elif line_len < self.line_len # decrease lines - for i: line_len .. self.line_len-1 - self.lines.remove(0) - end - end - self.line_len = line_len - end - - def _size_changed() - # print(">>> lv.EVENT_SIZE_CHANGED") - var pad_hor = self.get_style_pad_left(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_pad_right(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 - + 3 - var pad_ver = self.get_style_pad_top(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_pad_bottom(lv.PART_MAIN | lv.STATE_DEFAULT) - + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 - + 3 - var w = self.get_width() - pad_hor - var h = self.get_height() - pad_ver - self.label.set_size(w, h) - - # compute how many lines should be displayed - var h_font = lv.font_get_line_height(self.label.get_style_text_font(0)) # current font's height - var lines_count = ((h * 2 / h_font) + 1 ) / 2 - # print("h_font",h_font,"h",h,"lines_count",lines_count) - self.set_lines_count(lines_count) - end - - def size_changed_cb(obj, event) - var code = event.code - if code == lv.EVENT_SIZE_CHANGED || code == lv.EVENT_STYLE_CHANGED - self._size_changed() - elif code == lv.EVENT_DELETE - tasmota.remove_driver(self) - end - end - - def every_second() - var dirty = false - for n:0..20 - var line = self.log_reader.get_log(self.log_level) - if line == nil break end # no more logs - self.lines.remove(0) # remove first line - self.lines.push(line) - dirty = true - end - if dirty self.update() end - end - - def update() - var msg = self.lines.concat("\n") - self.label.set_text(msg) - end -end - -return lv_tasmota_log_roboto - -# import lv_tasmota_log -# var lg = lv_tasmota_log(scr, 6) -# lg.set_size(hres, 95) -# lg.set_pos(0, stat_line.get_height() + 40) -# tasmota.add_driver(lg) - -# var roboto12 = lv.font_robotocondensed_latin1(12) lg.set_style_text_font(roboto12, lv.PART_MAIN | lv.STATE_DEFAULT) \ No newline at end of file diff --git a/tasmota/berry/openhasp/openhasp.tapp b/tasmota/berry/openhasp/openhasp.tapp new file mode 100644 index 000000000..f664abf69 Binary files /dev/null and b/tasmota/berry/openhasp/openhasp.tapp differ diff --git a/tasmota/berry/openhasp/openhasp_widgets.tapp b/tasmota/berry/openhasp/openhasp_widgets.tapp new file mode 100644 index 000000000..45212df04 Binary files /dev/null and b/tasmota/berry/openhasp/openhasp_widgets.tapp differ diff --git a/tasmota/berry/openhasp/pages.jsonl b/tasmota/berry/openhasp/pages.jsonl new file mode 100644 index 000000000..59578d84a --- /dev/null +++ b/tasmota/berry/openhasp/pages.jsonl @@ -0,0 +1,33 @@ +{"page":0,"comment":"---------- Upper stat line ----------"} +{"id":0,"text_color":"#FFFFFF"} +{"id":11,"obj":"label","x":0,"y":0,"w":320,"pad_right":90,"h":22,"bg_color":"#D00000","bg_opa":255,"radius":0,"border_side":0,"text":"Tasmota","text_font":"montserrat-20"} + +{"id":15,"obj":"lv_wifi_arcs","x":291,"y":0,"w":29,"h":22,"radius":0,"border_side":0,"bg_color":"#000000","line_color":"#FFFFFF"} +{"id":16,"obj":"lv_clock","x":232,"y":3,"w":55,"h":16,"radius":0,"border_side":0} + +{"comment":"---------- Bottom buttons - prev/home/next ----------"} +{"id":101,"obj":"btn","x":20,"y":210,"w":80,"h":25,"action":"prev","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF053","text_font":"montserrat-20"} +{"id":102,"obj":"btn","x":120,"y":210,"w":80,"h":25,"action":"back","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF015","text_font":"montserrat-20"} +{"id":103,"obj":"btn","x":220,"y":210,"w":80,"h":25,"action":"next","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF054","text_font":"montserrat-20"} + +{"page":2,"comment":"---------- Page 2 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"comment":"---------- Wifi status ----------"} +{"id":20,"obj":"lv_wifi_graph","x":257,"y":25,"w":60,"h":40,"radius":0} +{"id":21,"obj":"lv_tasmota_info","x":3,"y":25,"w":251,"h":40,"radius":0} +{"id":22,"obj":"lv_tasmota_log","x":3,"y":68,"w":314,"h":90,"radius":0,"text_font":12} + +{"page":1,"comment":"---------- Page 1 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"--.-°C","min":200,"max":800,"val":0,"val_rule":"ESP32#Temperature","val_rule_formula":"val * 10","text_rule":"ESP32#Temperature","text_rule_format":"%2.1f °C"} + +{"id":5,"obj":"label","x":2,"y":35,"w":120,"text":"Temperature","align":1} + +{"id":10,"obj":"label","x":172,"y":35,"w":140,"text":"MPU","align":0} +{"id":11,"obj":"label","x":172,"y":55,"w":140,"text":"x=","align":0,"text_rule":"MPU9250#AX","text_rule_format":"x=%6.3f","text_rule_formula":"val / 1000"} +{"id":12,"obj":"label","x":172,"y":75,"w":140,"text":"y=","align":0,"text_rule":"MPU9250#AY","text_rule_format":"y=%6.3f","text_rule_formula":"val / 1000"} +{"id":13,"obj":"label","x":172,"y":95,"w":140,"text":"z=","align":0,"text_rule":"MPU9250#AZ","text_rule_format":"z=%6.3f","text_rule_formula":"val / 1000"} + +{"comment":"--- Trigger sensors every 2 seconds ---","berry_run":"tasmota.add_cron('*/2 * * * * *', def () tasmota.publish_rule(tasmota.read_sensors()) end, 'oh_every_5_s')"} diff --git a/tasmota/berry/openhasp/robotocondensed_latin1.tapp b/tasmota/berry/openhasp/robotocondensed_latin1.tapp new file mode 100644 index 000000000..e4c8ca84c Binary files /dev/null and b/tasmota/berry/openhasp/robotocondensed_latin1.tapp differ diff --git a/tasmota/berry/openhasp_src/openhasp.tapp b/tasmota/berry/openhasp_src/openhasp.tapp new file mode 100644 index 000000000..f664abf69 Binary files /dev/null and b/tasmota/berry/openhasp_src/openhasp.tapp differ diff --git a/tasmota/berry/openhasp_src/openhasp_core/autoexec.be b/tasmota/berry/openhasp_src/openhasp_core/autoexec.be new file mode 100644 index 000000000..61a215c5f --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_core/autoexec.be @@ -0,0 +1,7 @@ +# start openhasp +# package with +# zip -j -0 openhasp.tapp openhasp_core/* +import openhasp + +# defer start to make sure all modules and tapp applications were loaded first +tasmota.set_timer(1000, /-> openhasp.start()) diff --git a/tasmota/berry/openhasp_src/openhasp_core/openhasp.be b/tasmota/berry/openhasp_src/openhasp_core/openhasp.be new file mode 100644 index 000000000..85bae288f --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_core/openhasp.be @@ -0,0 +1,1489 @@ +# OpenHASP compatibility module +# +# use `import openhasp` and set the JSONL definitions in `pages.jsonl` +# +# As an optimization `0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#` is replaced with `0` +# +var openhasp = module("openhasp") + +################################################################################# +################################################################################# +# Class `lvh_obj` encapsulating `lv_obj`` +# +# Provide a mapping for virtual members +# Stores the associated page and object id +# +# Adds specific virtual members used by OpenHASP +################################################################################# +################################################################################# +class lvh_obj + static _lv_class = lv.obj # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses + static _lv_part2_selector # selector for secondary part (like knob of arc) + + # attributes to ignore when set at object level (they are managed by page) + static _attr_ignore = [ + "tostring", # avoid issues with Berry `tostring` method + # "id", + "obj", + "page", + "comment", + "parentid", + "auto_size", # TODO not sure it's still needed in LVGL8 + # attributes for page + "prev", "next", "back", + "berry_run", # run Berry code after the object is created + ] + + # The following defines the mapping between the JSONL attribute name + # and the Berry or LVGL attribute to set + # + # We try to map directly an attribute to the LVGL + # Ex: OpenHASP attribute `w` is mapped to LVGL `width` + # + # If mapping is null, we use set_X and get_X from our own class + static _attr_map = { + "x": "x", + "y": "y", + "w": "width", + "h": "height", + # arc + "asjustable": nil, + "mode": nil, + "start_angle": "bg_start_angle", + "start_angle1": "start_angle", + "end_angle": "bg_end_angle", + "end_angle1": "end_angle", + "radius": "style_radius", + "border_side": "style_border_side", + "border_width": "style_border_width", + "bg_opa": "style_bg_opa", + "border_width": "style_border_width", + "line_width": nil, # depends on class + "line_width1": nil, # depends on class + "action": nil, # store the action in self.action + "hidden": nil, # apply to self + "enabled": nil, # apply to self + "click": nil, # synonym to enabled + "toggle": nil, + "bg_color": "style_bg_color", + "bg_opa": "style_bg_opa", + "bg_grad_color": "style_bg_grad_color", + "bg_grad_dir": "style_bg_grad_dir", + "line_color": "style_line_color", + "pad_left": "style_pad_left", + "pad_right": "style_pad_right", + "pad_top": "style_pad_top", + "pad_bottom": "style_pad_bottom", + "pad_all": "style_pad_all", # write-only + "type": nil, + # below automatically create a sub-label + "text": nil, # apply to self + "value_str": nil, # synonym to 'text' + "align": nil, + "text_font": nil, + "value_font": nil, # synonym to text_font + "text_color": nil, + "value_color": nil, # synonym to text_color + "value_ofs_x": nil, + "value_ofs_y": nil, + # + "min": nil, + "max": nil, + "val": "value", + "rotation": "rotation", + # img + "src": "src", + "image_recolor": "style_img_recolor", + "image_recolor_opa": "style_img_recolor_opa", + # spinner + "angle": nil, + "speed": nil, + # padding of knob + "pad_top2": nil, + "pad_bottom2": nil, + "pad_left2": nil, + "pad_right2": nil, + "pad_all2": nil, + "radius2": nil, + # rule based update of attributes + # supporting both `val` and `text` + "val_rule": nil, + "val_rule_formula": nil, + "text_rule": nil, + "text_rule_formula": nil, + "text_rule_format": nil, + } + + #==================================================================== + # Instance variables + var id # (int) object hasp id + var _lv_obj # native lvgl object + var _lv_label # sub-label if exists + var _page # parent page object + var _action # value of the OpenHASP `action` attribute, shouldn't be called `self.action` since we want to trigger the set/member functions + + #==================================================================== + # Rule engine to map value and text to rules + # hence enabling auto-updates ob objects + var _val_rule # rule pattern to map the `val` attribute + var _val_rule_formula # Berry fragment to transform the value grabbed from rule + var _val_rule_function # compiled function + var _text_rule # rule pattern to map the `text` attribute + var _text_rule_formula # Berry fragment to transform the value grabbed from rule before string format + var _text_rule_function # compiled function + var _text_rule_format # string format to transform the value grabbed from rule + + ################################################################################# + # General utilities + # + ################################################################################# + # Checks if the attribute is a color + # I.e. ends with `color` (to not conflict with attributes containing `color_`) + ################################################################################# + static def is_color_attribute(t) + import re + return bool(re.search("color$", str(t))) + end + + ################################################################################# + # Parses a color attribute + # + # `parse_color(hex:string) -> color:int` (as 24 bits RGB int) + # + # Parses colors in multiple forms: + # - `0xRRGGBB` + # - `#RRGGBB` + # - `` that are matched to `lv.COLOR_` (ex: `red`) - case insensitive + # - defaults to black `0x000000` if parsing fails + ################################################################################# + static def parse_color(s) + # inner function + def parse_hex(s) + # parse hex string + # parse_hex(string) -> int + # skip any `#` prefix, or `0x` and `0X` prefix + import string + s = string.toupper(s) # turn to uppercase + var val = 0 + for i:0..size(s)-1 + var c = s[i] + # var c_int = string.byte(c) + if c == "#" continue end # skip '#' prefix if any + if c == "x" || c == "X" continue end # skip 'x' or 'X' + + if c >= "A" && c <= "F" + val = (val << 4) | string.byte(c) - 55 + elif c >= "0" && c <= "9" + val = (val << 4) | string.byte(c) - 48 + end + end + return val + end + + s = str(s) + if s[0] == '#' + return lv.color(parse_hex(s)) + else + import string + import introspect + var col_name = "COLOR_" + string.toupper(s) + var col_try = introspect.get(lv, col_name) + if col_try != nil + return lv.color(col_try) + end + end + # fail safe with black color + return lv.color(0x000000) + end + + #==================================================================== + # init OpenHASP object from its jsonl definition + # + # arg1: LVGL parent object (used to create a sub-object) + # arg2: `jline` JSONL definition of the object from OpenHASP template (used in sub-classes) + # arg3: (opt) LVGL object if it already exists and was created prior to init() + #==================================================================== + def init(parent, page, jline, obj) + self._page = page + if obj == nil && self._lv_class + var obj_class = self._lv_class # assign to a var to distinguish from method call + self._lv_obj = obj_class(parent) # instanciate LVGL object + else + self._lv_obj = obj + end + self.post_init() + end + + #==================================================================== + # post-init, to be overriden and used by certain classes + #==================================================================== + def post_init() + self.register_event_cb() + end + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # get LVGL encapsulated object + #==================================================================== + def get_obj() + return self._lv_obj + end + + #==================================================================== + # Value of the `action` attribute + #==================================================================== + def set_action(t) + self._action = str(t) + # add callback when clicked + # TODO + # self._lv_obj.add_event_cb(/ obj, event -> self.action_cb(obj, event), lv.EVENT_CLICKED, 0) + end + def get_action() + return self._action + end + + #==================================================================== + # Add cb for any action on the object + # + # Below is the mapping between HASP and LVGL (may need to adjust) + # down = LV_EVENT_PRESSED + # up = LV_EVENT_CLICKED + # lost = LV_EVENT_PRESS_LOST + # release = LV_EVENT_RELEASED + # long = LV_EVENT_LONG_PRESSED + # hold = LV_EVENT_LONG_PRESSED_REPEAT + # changed = LV_EVENT_VALUE_CHANGED + #==================================================================== + static _event_map = { + lv.EVENT_PRESSED: "down", + lv.EVENT_CLICKED: "up", + lv.EVENT_PRESS_LOST: "lost", + lv.EVENT_RELEASED: "release", + lv.EVENT_LONG_PRESSED: "long", + lv.EVENT_LONG_PRESSED_REPEAT: "hold", + lv.EVENT_VALUE_CHANGED: "changed", + } + def register_event_cb() + # register callback for each event + var f = / obj, event -> self.event_cb(obj, event) + for ev:self._event_map.keys() + self._lv_obj.add_event_cb(f, ev, 0) + end + # # print("register_event_cb") + # var mask = lv.EVENT_PRESSED | lv.EVENT_CLICKED | lv.EVENT_PRESS_LOST | lv.EVENT_RELEASED | + # lv.EVENT_LONG_PRESSED | lv.EVENT_LONG_PRESSED_REPEAT | lv.EVENT_VALUE_CHANGED + # var target = self + # mask = lv.EVENT_CLICKED + # self._lv_obj.add_event_cb(/ obj, event -> target.event_cb(obj, event), mask, 0) + end + + def event_cb(obj, event) + # the callback avoids doing anything sophisticated in the cb + # defer the actual action to the Tasmota event loop + # print("-> CB fired","self",self,"obj",obj,"event",event.tomap(),"code",event.code) + var oh = self._page._oh # openhasp global object + var code = event.code # materialize to a local variable, otherwise the value can change (and don't capture event object) + if self.action != nil && code == lv.EVENT_CLICKED + # if clicked and action is declared, do the page change event + tasmota.set_timer(0, /-> oh.do_action(self, code)) + end + + var event_hasp = self._event_map.find(code) + if event_hasp != nil + import string + var val = string.format('{"hasp":{"p%ib%i":"%s"}}', self._page._page_id, self.id, event_hasp) + # var pxby = "p" + self._page._page_id + "b" + self.id + # var val = '{"hasp":{"p' + str(self._page._page_id) + 'b' + str(self.id) + + # '":"' + event_hasp + '"}}' + # var val = json.dump( {'hasp': {pxby: event_hasp}} ) + # print("val=",val) + tasmota.set_timer(0, /-> tasmota.publish_rule(val)) + end + end + + # def action_cb(obj, event) + # # the callback avoids doing anything sophisticated in the cb + # # defer the actual action to the Tasmota event loop + # # print("-> CB fired","self",self,"obj",obj,"event",event.tomap(),"code",event.code) + # var oh = self._page._oh # openhasp global object + # var code = event.code # materialize to a local variable, otherwise the value can change (and don't capture event object) + # tasmota.set_timer(0, /-> oh.do_action(self, code)) + # end + + #==================================================================== + # Mapping of synthetic attributes + # - text + # - hidden + # - enabled + #==================================================================== + #- `hidden` attributes mapped to OBJ_FLAG_HIDDEN -# + def set_hidden(h) + if h + self._lv_obj.add_flag(lv.OBJ_FLAG_HIDDEN) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_HIDDEN) + end + end + def get_hidden() + return self._lv_obj.has_flag(lv.OBJ_FLAG_HIDDEN) + end + + #==================================================================== + # `enabled` attributes mapped to OBJ_FLAG_CLICKABLE + #==================================================================== + def set_enabled(h) + if h + self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) + end + end + def get_enabled() + return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) + end + + #==================================================================== + # click is synonym to enabled + #==================================================================== + def set_click(t) self.set_enabled(t) end + def get_click() return self.get_enabled() end + + #==================================================================== + # line_width + #==================================================================== + def set_line_width(t) + self._lv_obj.set_style_line_width(int(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_line_width() + return self._lv_obj.get_style_line_width(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + + #==================================================================== + # `toggle` attributes mapped to STATE_CHECKED + #==================================================================== + def set_toggle(t) + import string + t = string.toupper(str(t)) + if t == "TRUE" t = true end + if t == "FALSE" t = false end + if t + self._lv_obj.add_state(lv.STATE_CHECKED) + else + self._lv_obj.clear_state(lv.STATE_CHECKED) + end + end + def get_toggle() + return self._lv_obj.has_state(lv.STATE_CHECKED) + end + + #==================================================================== + # `adjustable` flag + #==================================================================== + def set_adjustable(t) + if t + self._lv_obj.add_flag(lv.OBJ_FLAG_CLICKABLE) + else + self._lv_obj.clear_flag(lv.OBJ_FLAG_CLICKABLE) + end + end + def get_adjustable() + return self._lv_obj.has_flag(lv.OBJ_FLAG_CLICKABLE) + end + + #==================================================================== + # set_text: create a `lv_label` sub object to the current object + # (default case, may be overriden by object that directly take text) + #==================================================================== + def check_label() + if self._lv_label == nil + self._lv_label = lv.label(self.get_obj()) + self._lv_label.set_align(lv.ALIGN_CENTER); + end + end + def set_text(t) + self.check_label() + self._lv_label.set_text(str(t)) + end + def set_value_str(t) self.set_text(t) end + def get_text() + if self._lv_label == nil return nil end + return self._lv_label.get_text() + end + def get_value_str() return self.get_text() end + + # mode + def set_mode(t) + var mode + if t == "expand" self._lv_obj.set_width(lv.SIZE_CONTENT) + elif t == "break" mode = lv.LABEL_LONG_WRAP + elif t == "dots" mode = lv.LABEL_LONG_DOT + elif t == "scroll" mode = lv.LABEL_LONG_SCROLL + elif t == "loop" mode = lv.LABEL_LONG_SCROLL_CIRCULAR + elif t == "crop" mode = lv.LABEL_LONG_CLIP + end + if mode != nil + self.check_label() + self._lv_label.set_long_mode(mode) + end + end + def get_mode() + end + + #==================================================================== + # `align`: `left`, `center`, `right` + #==================================================================== + def set_align(t) + var align + self.check_label() + if t == 0 || t == "left" + align = lv.TEXT_ALIGN_LEFT + elif t == 1 || t == "center" + align = lv.TEXT_ALIGN_CENTER + elif t == 2 || t == "right" + align = lv.TEXT_ALIGN_RIGHT + end + self._lv_label.set_style_text_align(align, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + + def get_align() + if self._lv_label == nil return nil end + var align self._lv_label.get_style_text_align(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + if align == lv.TEXT_ALIGN_LEFT + return "left" + elif align == lv.TEXT_ALIGN_CENTER + return "center" + elif align == lv.TEXT_ALIGN_RIGHT + return "right" + else + return nil + end + end + + #==================================================================== + # `text_font` + # + # For OpenHASP compatiblity, default to "robotocondensed-latin1" + # However we propose an extension to allow for other font names + # + # Arg1: (int) font size for `robotocondensed-latin1` + # or + # Arg1: (string) "font_name-font_size", ex: "montserrat-20" + #==================================================================== + def set_text_font(t) + # self.check_label() + var font + if type(t) == 'int' + font = lv.font_robotocondensed_latin1(t) + elif type(t) == 'string' + import string + var fn_split = string.split(t, '-') + if size(fn_split) >= 2 # it does contain '-' + var sz = int(fn_split[-1]) + var name = fn_split[0..-2].concat('-') # rebuild the font name + if sz > 0 && size(name) > 0 # looks good, let's have a try + try + font = lv.font_embedded(name, sz) + except .. + end + end + end + end + if font != nil + self._lv_obj.set_style_text_font(font, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + print("HSP: Unsupported font:", t) + end + end + def get_text_font() + end + def set_value_font(t) self.set_text_font(t) end + def get_value_font() return self.get_text_font() end + + #==================================================================== + # `text_color` + #==================================================================== + def set_text_color(t) + self._lv_obj.set_style_text_color(self.parse_color(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_text_color() + return self._lv_obj.get_style_text_color(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def set_value_color(t) self.set_text_color(t) end + def get_value_color() return self.get_value_color() end + + #==================================================================== + # `ofs_x`, `ofs_y` + #==================================================================== + def set_value_ofs_x(t) + self.check_label() + self._lv_label.set_x(int(t)) + end + def get_value_ofs_x() + return self._lv_label.get_x() + end + def set_value_ofs_y(t) + self.check_label() + self._lv_label.set_y(int(t)) + end + def get_value_ofs_y() + return self._lv_label.get_y() + end + + #==================================================================== + # `pad_top2`, `pad_bottom2`, `pad_left2`, `pad_right2`, `pad_alL2` + # secondary element + #==================================================================== + def set_pad_top2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_top(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_bottom2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_bottom(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_left2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_left(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_right2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_right(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def set_pad_all2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_pad_all(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + + #==================================================================== + # `pad_top`, `pad_bottom`, `pad_left`, `pad_right`, `pad_all` + #==================================================================== + def get_pad_top() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_top(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_bottom() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_bottom(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_left() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_left(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_right() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_pad_right(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_pad_all() + end + + #==================================================================== + # `radius2` + #==================================================================== + def set_radius2(t) + if self._lv_part2_selector != nil + self._lv_obj.set_style_radius(int(t), self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + def get_radius2() + if self._lv_part2_selector != nil + return self._lv_obj.get_style_radius(self._lv_part2_selector | lv.STATE_DEFAULT) + end + end + + #- ------------------------------------------------------------# + # Internal utility functions + # + # Mapping of virtual attributes + # + #- ------------------------------------------------------------# + # `member` virtual getter + #- ------------------------------------------------------------# + def member(k) + import string + # ignore attributes + # print("member","self=",self,"k=",k) + if self._attr_ignore.find(k) != nil return end + + # check if the key is known + if self._attr_map.contains(k) + # attribute is known + # kv: (if string) the LVGL attribute name of the object - direct mapping + # kv: (if `nil`) call `get_` method of the object + import introspect + var kv = self._attr_map[k] + + if kv == nil + # call the object's `get_X()` + var f = introspect.get(self, "get_" + k) # call self method + if type(f) == 'function' + return f(self) + end + else + # call the native LVGL object method + var f = introspect.get(self._lv_obj, "get_" + kv) + if type(f) == 'function' # found and function, call it + if string.find(kv, "style_") == 0 + # style function need a selector as second parameter + return f(self._lv_obj, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + return f(self._lv_obj) + end + end + end + end + # fallback to exception if attribute unknown or not a function + raise "value_error", "unknown attribute " + str(k) + end + + #- ------------------------------------------------------------# + # `setmember` virtual setter + #- ------------------------------------------------------------# + def setmember(k, v) + # print(">> setmember", k, v) + # print(">>", classname(self), self._attr_map) + # ignore attributes + if self._attr_ignore.find(k) != nil return end + + # is attribute known + if self._attr_map.contains(k) + import string + import introspect + var kv = self._attr_map[k] + # if a string is attached to the name, then set the corresponding LVGL attribute + if kv + var f = introspect.get(self._lv_obj, "set_" + kv) + # if the attribute contains 'color', convert to lv_color + if type(kv) == 'string' && self.is_color_attribute(kv) + v = self.parse_color(v) + end + # print("f=", f, v, kv, self._lv_obj, self) + if type(f) == 'function' + if string.find(kv, "style_") == 0 + # style function need a selector as second parameter + f(self._lv_obj, v, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + else + f(self._lv_obj, v) + end + return + else + print("HSP: Could not find function set_"+kv) + end + else + # else call the specific method from self + var f = introspect.get(self, "set_" + k) + # print("f==",f) + if type(f) == 'function' + f(self, v) + return + end + end + + else + print("HSP: unknown attribute:", k) + end + end + + #==================================================================== + # Rule based updates of `val` and `text` + # + # `val_rule`: rule pattern to grab a value, ex: `ESP32#Temperature` + # `val_rule_formula`: formula in Berry to transform the value + # Ex: `val * 10` + # `text_rule`: rule pattern to grab a value for text, ex: `ESP32#Temparature` + # `text_rule_format`: format used by `string.format()` + # Ex: `%.1f °C` + #==================================================================== + def set_val_rule(t) + # remove previous rule if any + if self._val_rule != nil + tasmota.remove_rule(self._val_rule, self) + end + + self._val_rule = str(t) + tasmota.add_rule(self._val_rule, / val -> self.val_rule_matched(val), self) + end + def get_val_rule() + return self._val_rule + end + # text_rule + def set_text_rule(t) + # remove previous rule if any + if self._text_rule != nil + tasmota.remove_rule(self._text_rule, self) + end + + self._text_rule = str(t) + tasmota.add_rule(self._text_rule, / val -> self.text_rule_matched(val), self) + end + def get_text_rule() + return self._text_rule + end + def set_text_rule_format(t) + self._text_rule_format = str(t) + end + def get_text_rule_format() + return self._text_rule_format + end + # formula that gets compiled as Berry code + def set_val_rule_formula(t) + self._val_rule_formula = str(t) + var code = "return / val -> (" + self._val_rule_formula + ")" + try + var func = compile(code) + self._val_rule_function = func() + except .. as e, m + import string + print(string.format("HSP: failed to compile '%s' - %s (%s)", code, e, m)) + end + end + def get_val_rule_formula() + return self._val_rule_formula + end + # formula that gets compiled as Berry code + def set_text_rule_formula(t) + self._text_rule_formula = str(t) + var code = "return / val -> (" + self._text_rule_formula + ")" + try + var func = compile(code) + self._text_rule_function = func() + except .. as e, m + import string + print(string.format("HSP: failed to compile '%s' - %s (%s)", code, e, m)) + end + end + def get_text_rule_formula() + return self._text_rule_formula + end + # rule matched for val + def val_rule_matched(val) + + # print(">> rule matched", "val=", val) + var val_n = real(val) # force float type + var func = self._val_rule_function + if func != nil + try + val_n = func(val_n) + except .. as e, m + import string + print(string.format("HSP: failed to run self._val_rule_function - %s (%s)", e, m)) + end + end + + self.val = int(val_n) # set value, truncate to int + return false # propagate the event further + end + # rule matched for text + def text_rule_matched(val) + + # print(">> rule matched text", "val=", val) + var val_n = real(val) # force float type + + var func = self._text_rule_function + if func != nil + try + val_n = func(val_n) + except .. as e, m + import string + print(string.format("HSP: failed to run self._text_rule_function - %s (%s)", e, m)) + end + end + + var format = self._text_rule_format + if type(format) == 'string' + import string + format = string.format(format, val_n) + else + format = "" + end + + self.text = format + return false # propagate the event further + end +end + +################################################################################# +# +# Other widgets +# +################################################################################# + +#==================================================================== +# label +#==================================================================== +class lvh_label : lvh_obj + static _lv_class = lv.label + # label do not need a sub-label + def post_init() + self._lv_label = self._lv_obj # the label is also the object itself + super(self).post_init() # call super + end +end + +#==================================================================== +# arc +#==================================================================== +class lvh_arc : lvh_obj + static _lv_class = lv.arc + static _lv_part2_selector = lv.PART_KNOB + + # line_width converts to arc_width + def set_line_width(t) + self._lv_obj.set_style_arc_width(int(t), 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def get_line_width() + return self._lv_obj.get_arc_line_width(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) + end + def set_line_width1(t) + self._lv_obj.set_style_arc_width(int(t), lv.PART_INDICATOR | lv.STATE_DEFAULT) + end + def get_line_width1() + return self._lv_obj.get_arc_line_width(lv.PART_INDICATOR | lv.STATE_DEFAULT) + end + + def set_min(t) + self._lv_obj.set_range(int(t), self.get_max()) + end + def set_max(t) + self._lv_obj.set_range(self.get_min(), int(t)) + end + def get_min() + return self._lv_obj.get_min_value() + end + def get_max() + return self._lv_obj.get_max_value() + end + def set_type(t) + var mode + if t == 0 mode = lv.ARC_MODE_NORMAL + elif t == 1 mode = lv.ARC_MODE_REVERSE + elif t == 2 mode = lv.ARC_MODE_SYMMETRICAL + end + if mode != nil + self._lv_obj.set_mode(mode) + end + end + def get_type() + return self._lv_obj.get_mode() + end +end + +#==================================================================== +# switch +#==================================================================== +class lvh_switch : lvh_obj + static _lv_class = lv.switch + static _lv_part2_selector = lv.PART_KNOB +end + +#==================================================================== +# spinner +#==================================================================== +class lvh_spinner : lvh_arc + static _lv_class = lv.spinner + + # init + # - create the LVGL encapsulated object + # arg1: parent object + # arg2: json line object + def init(parent, page, jline) + self._page = page + var angle = jline.find("angle", 60) + var speed = jline.find("speed", 1000) + self._lv_obj = lv.spinner(parent, speed, angle) + self.post_init() + end + + # ignore attributes, spinner can't be changed once created + def set_angle(t) end + def get_angle() end + def set_speed(t) end + def get_speed() end +end + +################################################################################# +# +# All other subclasses than just map the LVGL object +# and doesn't have any specific behavior +# +################################################################################# +class lvh_bar : lvh_obj static _lv_class = lv.bar end +class lvh_btn : lvh_obj static _lv_class = lv.btn end +class lvh_btnmatrix : lvh_obj static _lv_class = lv.btnmatrix end +class lvh_checkbox : lvh_obj static _lv_class = lv.checkbox end +class lvh_dropdown : lvh_obj static _lv_class = lv.dropdown end +class lvh_img : lvh_obj static _lv_class = lv.img end +class lvh_line : lvh_obj static _lv_class = lv.line end +class lvh_roller : lvh_obj static _lv_class = lv.roller end +class lvh_slider : lvh_obj static _lv_class = lv.slider end +class lvh_textarea : lvh_obj static _lv_class = lv.textarea end +# special case for scr (which is actually lv_obj) +class lvh_scr : lvh_obj static _lv_class = nil end # no class for screen + + +################################################################################# +# Class `lvh_page` +# +# Encapsulates a `lv_screen` which is `lv.obj(0)` object +################################################################################# +# +# ex of transition: lv.scr_load_anim(scr, lv.SCR_LOAD_ANIM_MOVE_RIGHT, 500, 0, false) +class lvh_page + var _obj_id # (map) of `lvh_obj` objects by id numbers + var _page_id # (int) id number of this page + var _lv_scr # (lv_obj) lvgl screen object + var _oh # OpenHASP global object + # openhasp attributes for page are on item `#0` + var prev, next, back # (int) id values for `prev`, `next`, `back` buttons + + #==================================================================== + # `init` + # + # arg1: `page_number` (int) OpenHASP page id + # defaults to `1` if not specified + # page 0 is special, visible on all pages. Internally uses `layer_top` + # arg2: `oh` global OpenHASP monad object + # page_number: openhasp page number, defaults to `1` if not specified + #==================================================================== + def init(page_number, oh) + import global + self._oh = oh # memorize OpenHASP parent object + + # if no parameter, default to page #1 + page_number = int(page_number) + if page_number == nil page_number = 1 end + + self._page_id = page_number # remember our page_number + self._obj_id = {} # init list of objects + + # initialize the LVGL object for the page + # uses a lv_scr object except for page 0 where we use layer_top + # page 1 is mapped directly to the default screen `scr_act` + if page_number == 1 + self._lv_scr = lv.scr_act() # default screen + elif page_number == 0 + self._lv_scr = lv.layer_top() # top layer, visible over all screens + else + self._lv_scr = lv.obj(0) # allocate a new screen + var bg_color = lv.scr_act().get_style_bg_color(0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) # bg_color of default screen + self._lv_scr.set_style_bg_color(bg_color, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#) # set white background + end + + # page object is also stored in the object map at id `0` as instance of `lvg_scr` + var obj_scr = lvh_scr(nil, self, nil, self._lv_scr) # store screen in a virtual object + self._obj_id[0] = obj_scr + + # create a global for this page of form p, ex `p1` + # create a global for the page attributes as pb0, ex `p1b0` + global.("p" + str(self._page_id)) = self + global.("p" + str(self._page_id) + "b0") = obj_scr + end + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # retrieve lvgl screen object for this page + #==================================================================== + def get_scr() + return self._lv_scr + end + + #==================================================================== + # return id of this page + #==================================================================== + def id() + return self._page_id + end + + #==================================================================== + # add an object to this page + #==================================================================== + def set_obj(id, o) + self._obj_id[id] = o + end + def get_obj(id) + return self._obj_id.find(id) + end + + #==================================================================== + # `show` transition from one page to another + # duration: in ms, default 500 ms + # anim: -1 right to left, 1 left to right (default) + # + # show this page, with animation + #==================================================================== + static show_anim = { + 1: lv.SCR_LOAD_ANIM_MOVE_LEFT, + -1: lv.SCR_LOAD_ANIM_MOVE_RIGHT, + -2: lv.SCR_LOAD_ANIM_MOVE_TOP, + 2: lv.SCR_LOAD_ANIM_MOVE_BOTTOM, + 0: lv.SCR_LOAD_ANIM_NONE, + } + def show(anim, duration) + # ignore if the page does not contain a screen, like when id==0 + if self._lv_scr == nil return nil end + + # ignore if the screen is already active + # compare native LVGL objects with current screen + if self._lv_scr._p == lv.scr_act()._p return end # do nothing + + # default duration of 500ms + if duration == nil duration = 500 end + + # if anim is `nil` try to guess the direction from current screen + if anim == nil + anim = self._oh.page_dir_to(self.id()) + end + + # change current page + self._oh.lvh_page_cur_idx = self._page_id + + var anim_lvgl = self.show_anim.find(anim, lv.SCR_LOAD_ANIM_NONE) + # load new screen with animation, no delay, 500ms transition time, no auto-delete + lv.scr_load_anim(self._lv_scr, anim_lvgl, duration, 0, false) + end +end + +################################################################################# +# +# class `OpenHASP` to initialize the OpenHASP parsing +# +################################################################################# + +# main class controller, meant to be a singleton and the only externally used class +class OpenHASP + var dark # (bool) use dark theme? + var hres, vres # (int) resolution + var scr # (lv_obj) default LVGL screen + var r16, r20 # (lv_font) robotocondensed fonts size 16 and 20 + # openhasp objects + var lvh_pages # (list of lvg_page) list of pages + var lvh_page_cur_idx # (int) current page index number + # regex patterns + var re_page_target # compiled regex for action `p` + + # assign lvh_page to a static attribute + static lvh_page = lvh_page + # assign all classes as static attributes + static lvh_btn = lvh_btn + static lvh_switch = lvh_switch + static lvh_checkbox = lvh_checkbox + static lvh_label = lvh_label + # static lvh_led = lvh_led + static lvh_spinner = lvh_spinner + static lvh_obj = lvh_obj + static lvh_line = lvh_line + static lvh_img = lvh_img + static lvh_dropdown = lvh_dropdown + static lvh_roller = lvh_roller + static lvh_btnmatrix = lvh_btnmatrix + # static lvh_msgbox = lvh_msgbox + # static lvh_tabview = lvh_tabview + # static lvh_tab = lvh_tab + # static lvh_cpiker = lvh_cpiker + static lvh_bar = lvh_bar + static lvh_slider = lvh_slider + static lvh_arc = lvh_arc + # static lvh_linemeter = lvh_linemeter + # static lvh_gauge = lvh_gauge + static lvh_textarea = lvh_textarea # additional? + + static def_templ_name = "pages.jsonl" # default template name + + def init() + import re + self.re_page_target = re.compile("p\\d+") + # nothing to put here up to now + end + + def deinit() + # remove previous rule if any + if self._val_rule != nil + tasmota.remove_rule(self._val_rule, self) + end + if self._text_rule != nil + tasmota.remove_rule(self._text_rule, self) + end + end + + #==================================================================== + # init + # + # arg1: (bool) use dark theme if `true` + # + # implicitly loads `pages.jsonl` from file-system // TODO allow to specicify file name + #==================================================================== + def start(dark, templ_name) + import path + if templ_name == nil templ_name = self.def_templ_name end + if !path.exists(templ_name) + raise "io_erorr", "file '" + templ_name + "' not found" + end + # start lv if not already started. It does no harm to call lv.start() if LVGL was already started + lv.start() + + self.dark = bool(dark) + + self.hres = lv.get_hor_res() # ex: 320 + self.vres = lv.get_ver_res() # ex: 240 + self.scr = lv.scr_act() # LVGL default screean object + + self.r20 = lv.font_robotocondensed_latin1(20) # // TODO what if does not exist + self.r16 = lv.font_robotocondensed_latin1(16) # // TODO what if does not exist + + # set the theme for OpenHASP + var th2 = lv.theme_openhasp_init(0, lv.color(0xFF00FF), lv.color(0x303030), self.dark, self.r16) + self.scr.get_disp().set_theme(th2) + self.scr.set_style_bg_color(self.dark ? lv.color(0x000000) : lv.color(0xFFFFFF),0) # set background to white + # apply theme to layer_top, but keep it transparent + lv.theme_apply(lv.layer_top()) + lv.layer_top().set_style_bg_opa(0,0) + + self.lvh_pages = {} + # load from JSONL + self._load(templ_name) + end + + ################################################################################# + # Simple insertion sort - sorts the list in place, and returns the list + ################################################################################# + static def sort(l) + # insertion sort + for i:1..size(l)-1 + var k = l[i] + var j = i + while (j > 0) && (l[j-1] > k) + l[j] = l[j-1] + j -= 1 + end + l[j] = k + end + return l + end + + + ##################################################################### + # General Setters and Getters + ##################################################################### + + #==================================================================== + # return the current page as `lvh_page` object + #==================================================================== + def get_page_cur() + return self.lvh_pages[self.lvh_page_cur_idx] + end + + #==================================================================== + # load JSONL template + #==================================================================== + def _load(templ_name) + import string + import json + #- pages -# + self.lvh_page_cur_idx = 1 + var lvh_page_class = self.lvh_page + self.lvh_pages[1] = lvh_page_class(1, self) # always create page #1 + + var f = open(templ_name,"r") + var jsonl = string.split(f.read(), "\n") + f.close() + + # parse each line + for j:jsonl + var jline = json.load(j) + + if type(jline) == 'instance' + self.parse_page(jline) # parse page first to create any page related objects, may change self.lvh_page_cur_idx + # objects are created in the current page + self.parse_obj(jline, self.lvh_pages[self.lvh_page_cur_idx]) # then parse object within this page + end + end + + # current page is always 1 when we start + self.lvh_page_cur_idx = 1 + end + + #==================================================================== + # `parse` + # + # Manually parse a single JSON line, after initial load + #==================================================================== + def parse(j) + import json + var jline = json.load(j) + + if type(jline) == 'instance' + self.parse_page(jline) # parse page first to create any page related objects, may change self.lvh_page_cur_idx + # objects are created in the current page + self.parse_obj(jline, self.lvh_pages[self.lvh_page_cur_idx]) # then parse object within this page + else + raise "value_error", "unable to parse JSON line" + end + end + + #==================================================================== + # `pages_list_sorted` + # + # Return the sorted list of page (ids without page 0) starting + # from the current page. + # Ex: if pages are [0,1,3,4,5,6] + # pages_list_sorted(4) -> [4,5,6,1,3] + # + # Arg1: number of current page, or `0` for current page, or `nil` to return just the list of pages + # Returns: list of ints, or nil if current page is not found + #==================================================================== + def pages_list_sorted(cur_page) + # get list of pages as sorted array + var pages = [] + if cur_page == 0 cur_page = self.lvh_page_cur_idx end + for p: self.lvh_pages.keys() + if p != 0 pages.push(p) end # discard page 0 + end + pages = self.sort(pages) + if cur_page == nil return cur_page end + + var count_pages = size(pages) # how many pages are defined + pages = pages + pages # double the list to splice it + var cur_idx = pages.find(cur_page) + if cur_idx == nil return nil end # internal error, current page not found + + pages = pages[cur_idx .. cur_idx + count_pages - 1] # splice the list + + return pages + end + + #==================================================================== + # `page_dir_to` + # + # Compute the best direction (right or left) to go from + # the current page to the destination page + # + # Returns: + # 1: scroll to the next page (right) + # 0: unknown + # -1: scroll to the prev page (left) + # -2: scroll to the home page (up or left) + #==================================================================== + def page_dir_to(to_page) + var sorted_pages_list = self.pages_list_sorted(0) # list of pages sorted by number, page 0 excluded + if sorted_pages_list == nil return 0 end + + var count_pages = size(sorted_pages_list) # how many pages are possible + if count_pages <= 1 return 0 end + # if we have 2 pages, then only 1 direction is possible + if count_pages == 2 return 1 end + # we have at least 3 pages + + var to_page_idx = sorted_pages_list.find(to_page) # find index of target page + if to_page_idx == nil return 0 end # target page not found + if to_page_idx <= (count_pages + 1) / 2 + return 1 + else + return -1 + end + end + + #==================================================================== + # Execute a page changing action from `action` attribute + # + # This is called in async mode after a button callback + # + # Arg1: lvh_X object that fired the action + # Arg2: LVGL event fired + # Returns: nil + #==================================================================== + def do_action(lvh_obj, event_code) + if event_code != lv.EVENT_CLICKED return end + var action = lvh_obj._action + var cur_page = self.lvh_pages[self.lvh_page_cur_idx] + # print("do_action","lvh_obj",lvh_obj,"action",action,"cur_page",cur_page,self.lvh_page_cur_idx) + + # action can be `prev`, `next`, `back`, or `p` like `p1` + var to_page = nil + var sorted_pages_list = self.pages_list_sorted(self.lvh_page_cur_idx) + if size(sorted_pages_list) <= 1 return end # if only 1 page, do nothing + # handle prev/next/back values + # get the corresponding value from page object, + # if absent, revert to next page, previous page and page 1 + # print("sorted_pages_list",sorted_pages_list) + if action == 'prev' + to_page = int(cur_page.prev) + if to_page == nil to_page = sorted_pages_list[-1] end # if no prev, take the previous page + elif action == 'next' + to_page = int(cur_page.next) + if to_page == nil to_page = sorted_pages_list[1] end # if no next, take the next page + elif action == 'back' + to_page = int(cur_page.back) + if to_page == nil to_page = 1 end # if no nack, take page number 1 + elif self.re_page_target.match(action) + # action is supposed to be `p` format + to_page = int(action[1..-1]) # just skip first char and convert the rest to a string + end + + # print("to_page=",to_page) + if to_page != nil && to_page > 0 # we have a target + self.lvh_pages[to_page].show() # switvh to the target page + end + end + + #==================================================================== + # Parse page information + # + # Create a new page object if required + # Change the active page + #==================================================================== + def parse_page(jline) + if jline.has("page") && type(jline["page"]) == 'int' + var page = int(jline["page"]) + self.lvh_page_cur_idx = page # change current page + + # create the page object if it doesn't exist already + if !self.lvh_pages.contains(page) + var lvh_page_class = self.lvh_page + self.lvh_pages[page] = lvh_page_class(page, self) + end + + # check if there is "id":0 + if jline.find("id") == 0 + var lvh_page_cur = self.get_page_cur() + lvh_page_cur.prev = int(jline.find("prev", nil)) + lvh_page_cur.next = int(jline.find("next", nil)) + lvh_page_cur.back = int(jline.find("back", nil)) + end + end + end + + #==================================================================== + # Parse single object + #==================================================================== + def parse_obj(jline, page) + import global + import string + import introspect + + var obj_id = int(jline.find("id")) # id number or nil + var obj_type = str(jline.find("obj")) # obj class or nil + var obj_lvh # lvgl object created + var lvh_page_cur = self.get_page_cur() # current page object + + # first run any Berry code embedded + var berry_run = str(jline.find("berry_run")) + if berry_run != "nil" + try + var func_compiled = compile(berry_run) + # run the compiled code once + func_compiled() + except .. as e,m + print(string.format("HSP: unable to run berry code \"%s\" - '%s' - %s", berry_run, e, m)) + end + end + + # if line contains botn 'obj' and 'id', create the object + if obj_id == nil return end # if no object id, ignore line + if obj_type != "nil" && obj_id != nil + # 'obj_id' must be between 1 and 254 + if obj_id < 1 || obj_id > 254 + print("HSP: invalid 'id': " + str(obj_id) + " for 'obj':" + obj_type) + return + end + + # extract openhasp class, prefix with `lvh_`. Ex: `btn` becomes `lvh_btn` + # extract parent + var parent_lvgl + var parent_id = int(jline.find("parentid")) + + if parent_id != nil + var parent_obj = lvh_page_cur.get_obj(parent_id) # get parent object + if parent_obj != nil parent_lvgl = parent_obj._lv_obj end # parent + end + if parent_lvgl == nil + parent_lvgl = lvh_page_cur.get_scr() # if not parent, use the current page screen + end + + # check if a class with the requested name exists + # first look for a class with name `lvh_` exists + var obj_class = introspect.get(self, "lvh_" + obj_type) + var lv_instance = nil # allows to pre-instanciate the object + + # there is no lvh_X class, try to load the class name from the global namespace + if obj_class == nil + # if not found, check if a LVGL class with name `lv_` exists + var lv_cl = introspect.get(global, obj_type) + if lv_cl != nil && type(lv_cl) == 'class' + lv_instance = lv_cl(parent_lvgl) + obj_class = lvh_obj # use the basic lvh_obj component to encapsulate + end + end + + # still not found, try to load a module with the name of the class + if obj_class == nil + var lv_cl = introspect.module(obj_type) + if lv_cl != nil && type(lv_cl) == 'class' + lv_instance = lv_cl(parent_lvgl) + obj_class = lvh_obj # use the basic lvh_obj component to encapsulate + end + end + + if obj_class == nil + print("HSP: cannot find object of type " + str(obj_type)) + return + end + + # instanciate the object, passing the lvgl screen as parent object + obj_lvh = obj_class(parent_lvgl, page, jline, lv_instance) + + # add object to page object + lvh_page_cur.set_obj(obj_id, obj_lvh) + + # create a global variable for this object of form pb, ex p1b2 + var glob_name = string.format("p%ib%i", lvh_page_cur.id(), obj_id) + global.(glob_name) = obj_lvh + end + + if obj_id == 0 && obj_type != "nil" + print("HSP: cannot specify 'obj' for 'id':0") + return + end + + # if id==0, retrieve the 'scr' object of the current page + if obj_id == 0 + obj_lvh = self.get_page_cur().get_obj(0) # get object id '0' + end + + # set attributes + # try every attribute, if not supported it is silently ignored + for k:jline.keys() + # introspect.set(obj, k, jline[k]) + obj_lvh.(k) = jline[k] + end + end +end +openhasp.OpenHASP = OpenHASP + +################################################################################# +# General module initilization +################################################################################# + +# automatically instanciate the OpenHASP() monad +# note: value is cached in the module cache +# and is returned whenever you call `import openhasp` again +# This means that the object is never garbage collected +# +openhasp.init = def (m) # `init(m)` is called during first `import openhasp` + return openhasp.OpenHASP() +end + +return openhasp diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo-all.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo-all.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo-all.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo-all.jsonl diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl similarity index 99% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl index 684e0d324..bc34ee5fb 100644 --- a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo1.jsonl +++ b/tasmota/berry/openhasp_src/openhasp_examples/demo1.jsonl @@ -1,5 +1,6 @@ {"page":1,"comment":"---------- Page 1 ----------"} {"page":1,"id":0,"bg_color":"#FFFFFF","bg_grad_color":"#FFFFFF","text_color":"#000000","radius":0,"border_side":0} + {"page":1,"id":1,"obj":"btn","x":0,"y":0,"w":240,"h":30,"text":"LIVING ROOM","value_font":22,"bg_color":"#2C3E50","bg_grad_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0} {"page":1,"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"max":40,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"21.2°C","min":-20,"max":50,"val":21} diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo2.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo2.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo2.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo2.jsonl diff --git a/lib/libesp32/berry_tasmota/src/embedded/openhasp/demo3.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/demo3.jsonl similarity index 100% rename from lib/libesp32/berry_tasmota/src/embedded/openhasp/demo3.jsonl rename to tasmota/berry/openhasp_src/openhasp_examples/demo3.jsonl diff --git a/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl b/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl new file mode 100644 index 000000000..a9af406b9 --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_examples/lv.jsonl @@ -0,0 +1,44 @@ +{"page":0,"comment":"---------- Upper stat line ----------"} +{"id":0,"text_color":"#FFFFFF"} +{"id":11,"obj":"label","x":0,"y":0,"w":320,"pad_right":90,"h":22,"bg_color":"#D00000","bg_opa":255,"radius":0,"border_side":0,"text":"Tasmota","text_font":"montserrat-20"} + +{"id":15,"obj":"lv_wifi_arcs","x":291,"y":0,"w":29,"h":22,"radius":0,"border_side":0,"bg_color":"#000000","line_color":"#FFFFFF"} +{"id":16,"obj":"lv_clock","x":232,"y":3,"w":55,"h":16,"radius":0,"border_side":0} + +{"comment":"---------- Bottom buttons - prev/home/next ----------"} +{"id":101,"obj":"btn","x":20,"y":210,"w":80,"h":25,"action":"prev","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF053","text_font":"montserrat-20"} +{"id":102,"obj":"btn","x":120,"y":210,"w":80,"h":25,"action":"back","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF015","text_font":"montserrat-20"} +{"id":103,"obj":"btn","x":220,"y":210,"w":80,"h":25,"action":"next","bg_color":"#1fa3ec","radius":10,"border_side":1,"text":"\uF054","text_font":"montserrat-20"} + +{"page":2,"comment":"---------- Page 2 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"comment":"---------- Wifi status ----------"} +{"id":20,"obj":"lv_wifi_graph","x":257,"y":25,"w":60,"h":40,"radius":0} +{"id":21,"obj":"lv_tasmota_info","x":3,"y":25,"w":251,"h":40,"radius":0} +{"id":22,"obj":"lv_tasmota_log","x":3,"y":68,"w":314,"h":90,"radius":0,"text_font":12} + +{"page":1,"comment":"---------- Page 1 ----------"} +{"id":0,"bg_color":"#0000A0","bg_grad_color":"#000000","bg_grad_dir":1,"text_color":"#FFFFFF"} + +{"id":2,"obj":"arc","x":20,"y":65,"w":80,"h":100,"border_side":0,"type":0,"rotation":0,"start_angle":180,"end_angle":0,"start_angle1":180,"value_font":12,"value_ofs_x":0,"value_ofs_y":-14,"bg_opa":0,"text":"--.-°C","min":200,"max":800,"val":0,"val_rule":"ESP32#Temperature","val_rule_formula":"val * 10","text_rule":"ESP32#Temperature","text_rule_format":"%2.1f °C"} + +{"id":5,"obj":"label","x":2,"y":35,"w":140,"text":"Temperature","align":1} + +{"id":10,"obj":"label","x":172,"y":35,"w":140,"text":"MPU","align":0} +{"id":11,"obj":"label","x":172,"y":55,"w":140,"text":"x=","align":0,"text_rule":"MPU9250#AX","text_rule_format":"x=%6.3f","text_rule_formula":"val / 1000"} +{"id":12,"obj":"label","x":172,"y":75,"w":140,"text":"y=","align":0,"text_rule":"MPU9250#AY","text_rule_format":"y=%6.3f","text_rule_formula":"val / 1000"} +{"id":13,"obj":"label","x":172,"y":95,"w":140,"text":"z=","align":0,"text_rule":"MPU9250#AZ","text_rule_format":"z=%6.3f","text_rule_formula":"val / 1000"} + + +{"page":3,"comment":"---------- Page 3 ----------"} +{"page":3,"id":1,"obj":"btn","x":0,"y":20,"w":240,"h":30,"text":"PAGE 3","text_font":16,"bg_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0,"click":0} + +{"page":3,"id":11,"obj":"img","src":"A:/noun_Fan_35097_140.png","auto_size":1,"w":140,"h":140,"x":50,"y":75,"image_recolor":"lime","image_recolor_opa":150} +{"page":3,"id":12,"obj":"spinner","parentid":11,"x":7,"y":6,"w":126,"h":126,"bg_opa":0,"border_width":0,"line_width":7,"line_width1":7,"type":2,"angle":120,"speed":1000,"value_str":3,"value_font":24} + +{"page":4,"comment":"---------- Page 4 ----------"} +{"page":4,"id":1,"obj":"btn","x":0,"y":20,"w":240,"h":30,"text":"PAGE 4","value_font":24,"bg_color":"#2C3E50","text_color":"#FFFFFF","radius":0,"border_side":0,"click":0} +{"page":4,"id":2,"obj":"obj","x":5,"y":35,"w":230,"h":250,"click":0} + +{"comment":"--- Trigger sensors every 2 seconds ---","berry_run":"tasmota.add_cron('*/2 * * * * *', def () tasmota.publish_rule(tasmota.read_sensors()) end, 'oh_every_5_s')"} diff --git a/tasmota/berry/openhasp_src/openhasp_widgets.tapp b/tasmota/berry/openhasp_src/openhasp_widgets.tapp new file mode 100644 index 000000000..45212df04 Binary files /dev/null and b/tasmota/berry/openhasp_src/openhasp_widgets.tapp differ diff --git a/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be b/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be new file mode 100644 index 000000000..edada704e --- /dev/null +++ b/tasmota/berry/openhasp_src/openhasp_widgets/autoexec.be @@ -0,0 +1,6 @@ +# pre-load widgets so future `import` will be already in memory +# create tapp file with: +# rm rm openhasp_widgets.tapp; zip -j -0 openhasp_widgets.tapp openhasp_widgets/* +import lv_tasmota_log +import lv_tasmota_info +import lv_wifi_graph diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_info.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_info.be similarity index 100% rename from tasmota/berry/lvgl_examples/lv_tasmota_info.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_info.be diff --git a/tasmota/berry/lvgl_examples/lv_tasmota_log.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be similarity index 87% rename from tasmota/berry/lvgl_examples/lv_tasmota_log.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be index 0bf2b42de..8db31f48b 100644 --- a/tasmota/berry/lvgl_examples/lv_tasmota_log.be +++ b/tasmota/berry/openhasp_src/openhasp_widgets/lv_tasmota_log.be @@ -24,14 +24,18 @@ class lv_tasmota_log : lv.obj self.refr_pos() self.label = lv.label(self) - self.label.set_width(self.get_width() - 12) self.label.set_style_text_color(lv.color(0x00FF00), lv.PART_MAIN | lv.STATE_DEFAULT) self.label.set_long_mode(lv.LABEL_LONG_CLIP) self.label.set_text("") # bug, still displays "Text" - self.add_event_cb( / obj, evt -> self.size_changed_cb(obj, evt), lv.EVENT_SIZE_CHANGED | lv.EVENT_STYLE_CHANGED | lv.EVENT_DELETE, 0) + self.label.set_width(self.get_width() - 12) + self.label.set_height(self.get_height() - 6) + self.add_event_cb( / -> self._size_changed(), lv.EVENT_SIZE_CHANGED, 0) + self.add_event_cb( / -> self._size_changed(), lv.EVENT_STYLE_CHANGED, 0) + self.add_event_cb( / -> tasmota.remove_driver(self), lv.EVENT_DELETE, 0) + self.lines = [] self.line_len = 0 self.log_reader = tasmota_log_reader() @@ -54,7 +58,7 @@ class lv_tasmota_log : lv.obj self.line_len = line_len end - def _size_changed() + def _size_changed(obj, evt) # print(">>> lv.EVENT_SIZE_CHANGED") var pad_hor = self.get_style_pad_left(lv.PART_MAIN | lv.STATE_DEFAULT) + self.get_style_pad_right(lv.PART_MAIN | lv.STATE_DEFAULT) @@ -64,7 +68,7 @@ class lv_tasmota_log : lv.obj + self.get_style_pad_bottom(lv.PART_MAIN | lv.STATE_DEFAULT) + self.get_style_border_width(lv.PART_MAIN | lv.STATE_DEFAULT) * 2 + 3 - var w = self.get_width() - pad_hor + var w = self.get_width() - pad_hor - 2 var h = self.get_height() - pad_ver self.label.set_size(w, h) # print("w",w,"h",h,"pad_hor",pad_hor,"pad_ver",pad_ver) @@ -76,15 +80,6 @@ class lv_tasmota_log : lv.obj self.set_lines_count(lines_count) end - def size_changed_cb(obj, event) - var code = event.code - if code == lv.EVENT_SIZE_CHANGED || code == lv.EVENT_STYLE_CHANGED - self._size_changed() - elif code == lv.EVENT_DELETE - tasmota.remove_driver(self) - end - end - def every_second() var dirty = false for n:0..20 diff --git a/tasmota/berry/lvgl_examples/lv_wifi_graph.be b/tasmota/berry/openhasp_src/openhasp_widgets/lv_wifi_graph.be similarity index 100% rename from tasmota/berry/lvgl_examples/lv_wifi_graph.be rename to tasmota/berry/openhasp_src/openhasp_widgets/lv_wifi_graph.be