Tasmota/lib/libesp32_lvgl/lv_haspmota/src/embedded/lv_haspmota.be

3364 lines
112 KiB
Plaintext

# HASPMota - OpenHASP compatibility module
#
# use `import haspmota` and set the JSONL definitions in `pages.jsonl`
#
# As an optimization `0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#` is replaced with `0`
#
# rm haspmota.tapp; zip -j -0 haspmota.tapp haspmota_core/*
#################################################################################
var haspmota = module("haspmota")
#################################################################################
# Bytes list
#
# This function takes a list of events (uin8) and returns a bytes object
#
# It is used only at compile time, and is not included in the final flash
# The bytes object is far more compact than a list of ints and
# does automatic deduplication if the same list occurs twice
#################################################################################
def list_to_bytes(l)
var b = bytes()
for v: l
b.add(v, 1)
end
return b
end
#################################################################################
# Pre-defined events lists
#################################################################################
var EVENTS_NONE = list_to_bytes([])
var EVENTS_TOUCH = list_to_bytes([lv.EVENT_PRESSED, lv.EVENT_CLICKED, lv.EVENT_PRESS_LOST, lv.EVENT_RELEASED,
lv.EVENT_LONG_PRESSED, #-lv.EVENT_LONG_PRESSED_REPEAT-# ])
var EVENTS_ALL = list_to_bytes([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 ]) # adding VALUE_CHANGED
#################################################################################
# Class `lvh_root`
#
# Allows to map either a `lv_obj` for LVGL or arbitrary object
#
#################################################################################
#@ solidify:lvh_root,weak
class lvh_root
static var _lv_class = nil # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses
static var _EVENTS = EVENTS_NONE
# attributes to ignore when set at object level (they are managed by page)
static var _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: HASPmota attribute `w` is mapped to LVGL `width`
#
# If mapping is null, we use set_X and get_X from our own class
static var _attr_map = {
"w": "width",
"h": "height",
"start_angle": "bg_start_angle",
"start_angle1": "start_angle",
"end_angle": "bg_end_angle",
"end_angle1": "end_angle",
}
#====================================================================
# Instance variables
var id # (int) object hasp id
var _lv_obj # native lvgl object
var _page # parent page object
var _parent_lvh # parent HASPmota object if 'parentid' was set, or 'nil'
var _meta # free form metadata
var _tag # free-form JSON tag
#====================================================================
# Rule engine to map value and text to rules
# hence enabling auto-updates ob objects
var _val # last known value, useful for graceful initialization
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_<x>`)
#################################################################################
def is_color_attribute(t)
import string
return string.endswith(str(t), "color")
end
#- remove trailing NULL chars from a bytes buffer before converting to string -#
#- Berry strings can contain NULL, but this messes up C-Berry interface -#
static def remove_trailing_zeroes(b)
var sz = size(b)
var i = 0
while i < sz
if b[-1-i] != 0 break end
i += 1
end
if i > 0
b.resize(size(b)-i)
end
return b
end
#====================================================================
# `delete` special attribute used to delete the object
#====================================================================
def set_delete(v)
raise "type_error", "you cannot assign to 'delete'"
end
def get_delete()
self._delete()
return def () end
end
def _delete()
# remove from page
self._page.remove_obj(self.id)
end
#################################################################################
# Parses a color attribute
#
# `parse_color(hex:string) -> color:int` (as 24 bits RGB int)
#
# Parses colors in multiple forms:
# - `0xRRGGBB`
# - `#RRGGBB`
# - `<color_name>` that are matched to `lv.COLOR_<color_name>` (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
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 >= "a" && c <= "f"
val = (val << 4) | string.byte(c) - 87
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
#====================================================================
# parse_font
#
# For HASPmota 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 parse_font(t)
var font
if type(t) == 'int'
try
font = lv.font_embedded("robotocondensed", t)
except ..
import path
# try TTF file "roboto.ttf" or "RobotoCondensed-Regular.ttf"
try
var ttf_name = "roboto.ttf"
if !path.exists(ttf_name)
ttf_name = "RobotoCondensed-Regular.ttf"
if !path.exists(ttf_name)
ttf_name = nil
end
end
if ttf_name != nil
font = lv.load_freetype_font(ttf_name, t, 0)
else
print("HSP: 'roboto.ttf' file missing for size:", t)
return nil
end
except ..
print("HSP: Unsupported font:", t)
return nil
end
end
elif type(t) == 'string'
import string
# look for 'A:name.font' style font file name
var drive_split = string.split(t, ':', 1)
var fn_split = string.split(t, '-')
var name = t
var sz = 0
var is_ttf = false
var is_binary = (size(drive_split) > 1 && size(drive_split[0]))
if size(fn_split) >= 2
sz = int(fn_split[-1])
name = fn_split[0..-2].concat('-') # rebuild the font name
end
if string.endswith(name, ".ttf", true)
# ttf font
name = string.split(name, ':')[-1] # remove A: if any
is_ttf = true
end
if is_ttf
font = lv.load_freetype_font(name, sz, 0)
elif is_binary
# font is from disk
font = lv.load_font(t)
elif sz > 0 && size(name) > 0 # looks good, let's have a try
try
font = lv.font_embedded(name, sz)
except ..
print("HSP: Unsupported font:", t)
end
end
end
if font != nil
return font
else
print("HSP: Unsupported font:", t)
end
end
#====================================================================
# init HASPmota object from its jsonl definition
#
# parent: LVGL parent object (used to create a sub-object)
# page: HASPmota page object
# jline: JSONL definition of the object from HASPmota template (used in sub-classes)
# obj: (opt) LVGL object if it already exists and was created prior to init()
# parent_lvh: HASPmota parent object defined by `parentid`
#====================================================================
def init(parent, page, jline, obj, parent_lvh)
self._page = page
self._parent_lvh = parent_lvh
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
#====================================================================
# called once all attributes have been parsed
# and gives an opportunity to clean up or refresh
#====================================================================
def post_config()
# set again value, because the first time the range might not have been valid
if (self._val != nil)
self.set_val(self._val)
end
end
#####################################################################
# General Setters and Getters
#####################################################################
#====================================================================
# get LVGL encapsulated object
#====================================================================
def get_obj()
return self._lv_obj
end
#====================================================================
# set_tag: create a free-form JSON tag
#====================================================================
def set_tag(t)
self._tag = t
end
def get_tag()
return self._tag
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 set_text(t)
end
def set_value_str(t) self.set_text(t) end
def get_text()
return nil
end
def get_value_str() return self.get_text() end
#- ------------------------------------------------------------#
# `digits_to_style`
#
# Convert a 2 digits style descriptor to LVGL style modifier
# See https://www.openhasp.com/0.6.3/design/styling/
#
#
# 00 = main part of the object (i.e. the background)
# 10 = the indicator or needle, highlighting the the current value
# 20 = the knob which can be used the change the value
# 30 = the background of the items/buttons
# 40 = the items/buttons
# 50 = the selected item
# 60 = major ticks of the gauge object
# 70 = the text cursor
# 80 = the scrollbar
# 90 = other special part, not listed above
#
# LV_PART_MAIN = 0x000000, /**< A background like rectangle*/
# LV_PART_SCROLLBAR = 0x010000, /**< The scrollbar(s)*/
# LV_PART_INDICATOR = 0x020000, /**< Indicator, e.g. for slider, bar, switch, or the tick box of the checkbox*/
# LV_PART_KNOB = 0x030000, /**< Like handle to grab to adjust the value*/
# LV_PART_SELECTED = 0x040000, /**< Indicate the currently selected option or section*/
# LV_PART_ITEMS = 0x050000, /**< Used if the widget has multiple similar elements (e.g. table cells)*/
# LV_PART_CURSOR = 0x060000, /**< Mark a specific place e.g. for text area's cursor or on a chart*/
# LV_PART_CUSTOM_FIRST = 0x080000, /**< Extension point for custom widgets*/
# LV_PART_ANY = 0x0F0000, /**< Special value can be used in some functions to target all parts*/
#
# 00 = default styling
# 01 = styling for toggled state
# 02 = styling for pressed, not toggled state
# 03 = styling for pressed and toggled state
# 04 = styling for disabled not toggled state
# 05 = styling for disabled and toggled state
#
# LV_STATE_DEFAULT = 0x0000,
# LV_STATE_CHECKED = 0x0001,
# LV_STATE_FOCUSED = 0x0002,
# LV_STATE_FOCUS_KEY = 0x0004,
# LV_STATE_EDITED = 0x0008,
# LV_STATE_HOVERED = 0x0010,
# LV_STATE_PRESSED = 0x0020,
# LV_STATE_SCROLLED = 0x0040,
# LV_STATE_DISABLED = 0x0080,
# LV_STATE_USER_1 = 0x1000,
# LV_STATE_USER_2 = 0x2000,
# LV_STATE_USER_3 = 0x4000,
# LV_STATE_USER_4 = 0x8000,
#
#- ------------------------------------------------------------#
static var _digit2part = [
lv.PART_MAIN, # 00
lv.PART_INDICATOR, # 10
lv.PART_KNOB, # 20
lv.PART_ITEMS, # 30 TODO
lv.PART_ITEMS, # 40
lv.PART_SELECTED, # 50
lv.PART_ITEMS, # 60
lv.PART_CURSOR, # 70
lv.PART_SCROLLBAR, # 80
lv.PART_CUSTOM_FIRST, # 90
]
static var _digit2state = [
lv.STATE_DEFAULT, # 00
lv.STATE_CHECKED, # 01
lv.STATE_PRESSED, # 02
lv.STATE_CHECKED | lv.STATE_PRESSED, # 03
lv.STATE_DISABLED, # 04
lv.STATE_DISABLED | lv.STATE_PRESSED, # 05
]
def digits_to_style(digits)
if digits == nil return 0 end # lv.PART_MAIN | lv.STATE_DEFAULT
var first_digit = (digits / 10) % 10
var second_digit = digits % 10
var val = 0 # lv.PART_MAIN | lv.STATE_DEFAULT
if first_digit >= 0 && first_digit < size(self._digit2part)
val = val | self._digit2part[first_digit]
else
val = nil
end
if second_digit >= 0 && second_digit < size(self._digit2state)
val = val | self._digit2state[second_digit]
else
val = nil
end
if val == nil
raise "value_error", f"invalid style suffix {digits:02i}"
end
return val
end
#====================================================================
# Metadata
#
#====================================================================
def set_meta(t)
self._meta = t
end
def get_meta()
return self._meta
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 `format()`
# Ex: `%.1f °C`
#====================================================================
def remove_val_rule()
if self._val_rule != nil
tasmota.remove_rule(self._val_rule, self)
end
end
def set_val_rule(t)
# remove previous rule if any
self.remove_val_rule()
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 remove_text_rule()
if self._text_rule != nil
tasmota.remove_rule(self._text_rule, self)
end
end
def set_text_rule(t)
# remove previous rule if any
self.remove_text_rule()
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
print(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
print(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
if val_n == nil return false end # if the matched value is not a number, ignore
var func = self._val_rule_function
if func != nil
try
val_n = func(val_n)
except .. as e, m
print(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)
if type(val) == 'int'
val = real(val) # force float type
end
var func = self._text_rule_function
if func != nil
try
val = func(val)
except .. as e, m
print(format("HSP: failed to run self._text_rule_function - %s (%s)", e, m))
end
end
var fmt = self._text_rule_format
if type(fmt) == 'string'
fmt = format(fmt, val)
else
fmt = ""
end
self.text = fmt
return false # propagate the event further
end
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 HASPmota
#################################################################################
#################################################################################
#@ solidify:lvh_obj,weak
class lvh_obj : lvh_root
static var _lv_class = lv.obj # _lv_class refers to the lvgl class encapsulated, and is overriden by subclasses
static var _lv_part2_selector # selector for secondary part (like knob of arc)
static var _EVENTS = EVENTS_ALL
#====================================================================
# Instance variables
var _lv_label # sub-label if exists
var _action # value of the HASPmota `action` attribute, shouldn't be called `self.action` since we want to trigger the set/member functions
#====================================================================
# init HASPmota object from its jsonl definition
#
# parent: LVGL parent object (used to create a sub-object)
# page: HASPmota page object
# jline: JSONL definition of the object from HASPmota template (used in sub-classes)
# obj: (opt) LVGL object if it already exists and was created prior to init()
# parent_lvh: HASPmota parent object defined by `parentid`
#====================================================================
def init(parent, page, jline, obj, parent_lvh)
super(self).init(parent, page, jline, obj, parent_lvh)
end
#====================================================================
# post-init, to be overriden and used by certain classes
#====================================================================
def post_init()
self.register_event_cb()
end
#####################################################################
# General Setters and Getters
#####################################################################
#====================================================================
# 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()
var action = self._action
return action ? action : "" # cannot be `nil` as it would mean no member
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 var _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()
var hm = self._page._hm
var b = self._EVENTS
var i = 0
while (i < size(b))
hm.register_event(self, b[i])
i += 1
end
end
def event_cb(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 hm = self._page._hm # haspmota global object
var code = event.get_code() # materialize to a local variable, otherwise the value can change (and don't capture event object)
if self.action != "" && code == lv.EVENT_CLICKED
# if clicked and action is declared, do the page change event
tasmota.set_timer(0, /-> hm.do_action(self, code))
end
var event_hasp = self._event_map.find(code)
if event_hasp != nil
import json
var tas_event_more = "" # complementary data
if code == lv.EVENT_VALUE_CHANGED
import introspect
var val = introspect.get(self, "val") # does not raise an exception if not found
if (val != nil && type(val) != 'module')
tas_event_more = f',"val":{json.dump(val)}'
end
var text = introspect.get(self, "text") # does not raise an exception if not found
if (text != nil && type(text) != 'module')
tas_event_more += f',"text":{json.dump(text)}'
end
end
# add tag if present
if (self._tag != nil)
tas_event_more += f',"tag":{json.dump(self._tag)}'
end
var tas_event = format('{"hasp":{"p%ib%i":{"event":"%s"%s}}}', self._page._page_id, self.id, event_hasp, tas_event_more)
# print("val=",val)
tasmota.set_timer(0, def ()
tasmota.publish_rule(tas_event)
tasmota.log(f"HSP: publish {tas_event}", 4)
end)
end
end
#====================================================================
# `_delete` special attribute used to delete the object
#====================================================================
# the actual _delete method, overriden
def _delete()
# remove any rule
self.remove_val_rule()
self.remove_text_rule()
if (self._lv_label) self._lv_label.del() self._lv_label = nil end
if (self._lv_obj) self._lv_obj.del() self._lv_obj = nil end
super(self)._delete()
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 STATE_DISABLED
#====================================================================
def set_enabled(h)
if h
self._lv_obj.clear_state(lv.STATE_DISABLED)
else
self._lv_obj.add_state(lv.STATE_DISABLED)
end
end
def get_enabled()
return !self._lv_obj.has_state(lv.STATE_DISABLED)
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, style_modifier)
self._lv_obj.set_style_line_width(int(t), style_modifier)
end
def get_line_width(style_modifier)
return self._lv_obj.get_style_line_width(style_modifier)
end
#====================================================================
# `toggle` attributes mapped to STATE_CHECKED
#====================================================================
def set_toggle(t)
import string
if type(t) == 'string'
t = string.toupper(str(t))
if t == "TRUE" t = true
elif t == "FALSE" t = false
end
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
import introspect
if introspect.contains(self._lv_obj, "set_text")
# if the `set_text` method exist, then use the native label
self._lv_label = self._lv_obj
else
# otherwise create a sub-label object
self._lv_label = lv.label(self.get_obj())
self._lv_label.set_align(lv.ALIGN_CENTER);
end
end
end
def set_text(t)
self.check_label()
self._lv_label.set_text(str(t))
end
def get_text()
if self._lv_label == nil return nil end
return self._lv_label.get_text()
end
# mode
def set_label_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_label_mode()
if self._lv_label != nil
return self._lv_label.get_long_mode()
end
end
#====================================================================
# `align`: `left`, `center`, `right`
#====================================================================
def set_align(t, m)
var align
if (m == 0) m = 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -# end
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, m)
end
def get_align(m)
if (m == 0) m = 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -# end
if self._lv_label == nil return nil end
var align = self._lv_label.get_style_text_align(m)
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 align
end
end
#====================================================================
# flex new_track
#
# Force flex child on a new line
#====================================================================
def set_flex_in_new_track(t)
if t
self._lv_obj.add_flag(lv.OBJ_FLAG_FLEX_IN_NEW_TRACK)
else
self._lv_obj.clear_flag(lv.OBJ_FLAG_FLEX_IN_NEW_TRACK)
end
end
def get_flex_in_new_track()
return self._lv_obj.has_flag(lv.OBJ_FLAG_FLEX_IN_NEW_TRACK)
end
#====================================================================
# `text_font`
#
# For HASPmota 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)
var font = self.parse_font(t)
if font != nil
self._lv_obj.set_style_text_font(font, 0 #- lv.PART_MAIN | lv.STATE_DEFAULT -#)
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, style_modifier)
self._lv_obj.set_style_text_color(self.parse_color(t), style_modifier)
end
def get_text_color(style_modifier)
return self._lv_obj.get_style_text_color(style_modifier)
end
def set_value_color(t, style_modifier) self.set_text_color(t, style_modifier) 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
def set_val(t)
import introspect
self._val = t
if introspect.contains(self._lv_obj, "set_value")
self._lv_obj.set_value(t)
end
end
def get_val()
return self._lv_obj.get_value()
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
import introspect
if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# check if the attribute ends with 2 digits, if so remove the two suffix digits
var style_modifier = nil
if size(k) >= 3
var char_last_1 = string.byte(k[-1])
var char_last_2 = string.byte(k[-2])
if (char_last_1 >= 0x30 && char_last_1 <= 0x39 && char_last_2 >= 0x30 && char_last_2 <= 0x39)
# we extract the last 2 digits
var suffix_digits = int(k[-2..])
k = k[0..-3] # remove 2 last digits
style_modifier = self.digits_to_style(suffix_digits)
end
end
# print(f">>>: getmember {k=} {style_modifier=}")
# if attribute name is in ignore list, abort
if self._attr_ignore.find(k) != nil return end
# first check if there is a method named `get_X()`
var f = introspect.get(self, "get_" + k) # call self method
if type(f) == 'function'
# print(f">>>: getmember local method get_{k}")
return f(self, style_modifier != nil ? style_modifier : 0)
end
# apply any synonym from _attr_map
k = self._attr_map.find(k, k)
# try first `get_X` from lvgl object, only if there is no style modifier
if (style_modifier == nil)
f = introspect.get(self._lv_obj, "get_" + k)
if type(f) == 'function' # found and function, call it
# print(f">>>: getmember standard method get_{k}")
return f(self._lv_obj)
end
end
# if not found, try `get_style_X`
f = introspect.get(self._lv_obj, "get_style_" + k)
if type(f) == 'function' # found and function, call it
# print(f">>>: getmember style_ method get_{k}")
# style function need a selector as second parameter
return f(self._lv_obj, style_modifier != nil ? style_modifier : 0)
end
# fallback to exception if attribute unknown or not a function
return module("undefined")
end
#- ------------------------------------------------------------#
# `setmember` virtual setter
#- ------------------------------------------------------------#
def setmember(k, v)
import string
import introspect
if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# if value is 'real', round to nearest int
if type(v) == 'real'
import math
v = int(math.round(v))
end
# parse value in percentage
if string.endswith(k, "%")
k = k[0..-2]
v = lv.pct(int(v))
end
# check if the attribute ends with 2 digits, if so remove the two suffix digits
var style_modifier = nil
if size(k) >= 3
var char_last_1 = string.byte(k[-1])
var char_last_2 = string.byte(k[-2])
if (char_last_1 >= 0x30 && char_last_1 <= 0x39 && char_last_2 >= 0x30 && char_last_2 <= 0x39)
# we extract the last 2 digits
var suffix_digits = int(k[-2..])
k = k[0..-3] # remove 2 last digits
style_modifier = self.digits_to_style(suffix_digits)
end
end
# print(f">>>: setmember {k=} {style_modifier=}")
# if attribute name is in ignore list, abort
if self._attr_ignore.find(k) != nil return end
# first check if there is a method named `set_X()`
var f = introspect.get(self, "set_" + k)
if type(f) == 'function'
# print(f">>>: setmember local method set_{k}")
f(self, v, style_modifier != nil ? style_modifier : 0)
return
end
# apply any synonym from _attr_map
k = self._attr_map.find(k, k)
# if the attribute contains 'color', convert to lv_color
if self.is_color_attribute(k)
v = self.parse_color(v)
end
# try first `set_X` from lvgl object
if (style_modifier == nil)
f = introspect.get(self._lv_obj, "set_" + k)
if type(f) == 'function' # found and function, call it
# print(f">>>: setmember standard method set_{k}")
return f(self._lv_obj, v)
end
end
# if not found, try `set_style_X`
f = introspect.get(self._lv_obj, "set_style_" + k)
if type(f) == 'function' # found and function, call it
# print(f">>>: setmember style_ method set_{k}")
# style function need a selector as second parameter
return f(self._lv_obj, v, style_modifier != nil ? style_modifier : 0)
end
print("HSP: unknown attribute:", k)
end
end
#################################################################################
#
# Other widgets
#
#################################################################################
#====================================================================
# fixed - container for a box with fixed sub-objects (no placement)
#====================================================================
#@ solidify:lvh_fixed,weak
class lvh_fixed : lvh_obj
# static var _lv_class = lv.obj # from parent class
# static var _EVENTS = EVENTS_ALL
# label do not need a sub-label
def post_init()
super(self).post_init() # call super
var obj = self._lv_obj
obj.set_style_pad_all(0, 0)
obj.set_style_radius(0, 0)
obj.set_style_border_width(0, 0)
obj.set_style_margin_all(0, 0)
obj.set_style_bg_opa(0, 0)
obj.set_size(lv.pct(100), lv.pct(100))
end
end
#====================================================================
# flex - container for sub-objects placed along with flex directives
#====================================================================
#@ solidify:lvh_flex,weak
class lvh_flex : lvh_fixed
# static var _lv_class = lv.obj # from parent class
static var _EVENTS = EVENTS_NONE # inhetited
# label do not need a sub-label
def post_init()
super(self).post_init() # call super
var obj = self._lv_obj
obj.set_flex_flow(lv.FLEX_FLOW_ROW)
end
end
#====================================================================
# label
#====================================================================
#@ solidify:lvh_label,weak
class lvh_label : lvh_obj
static var _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
#====================================================================
#@ solidify:lvh_arc,weak
class lvh_arc : lvh_obj
static var _lv_class = lv.arc
static var _lv_part2_selector = lv.PART_KNOB
# static var _EVENTS = EVENTS_ALL
var _label_angle # nil if none
# line_width converts to arc_width
def set_line_width(t, style_modifier)
self._lv_obj.set_style_arc_width(int(t), style_modifier)
end
def get_line_width(style_modifier)
return self._lv_obj.get_arc_line_width(style_modifier)
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())
self.refresh_label_to_angle()
end
def set_max(t)
self._lv_obj.set_range(self.get_min(), int(t))
self.refresh_label_to_angle()
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
# force refresh after set_val
def set_val(t)
super(self).set_val(t)
self.refresh_label_to_angle()
end
# force refresh after set_text
def set_text(t)
super(self).set_text(t)
self.refresh_label_to_angle()
end
#====================================================================
# `label_to_angle`
# ability to turn and offset the label.
# value is the offset in pixels to move the label
def set_label_to_angle(t)
self._label_angle = int(t)
self.refresh_label_to_angle()
end
def refresh_label_to_angle()
if (self._label_angle != nil && self._lv_label != nil)
self._lv_obj.rotate_obj_to_angle(self._lv_label, self._label_angle)
end
end
# update after parsing
def post_config()
super(self).post_config() # not needed yet
self.refresh_label_to_angle()
end
end
#====================================================================
# switch
#====================================================================
#@ solidify:lvh_switch,weak
class lvh_switch : lvh_obj
static var _lv_class = lv.switch
static var _lv_part2_selector = lv.PART_KNOB
# static var _EVENTS = EVENTS_ALL
# map val to toggle
def set_val(t)
self._val = t
return self.set_toggle(t)
end
def get_val()
return self.get_toggle()
end
end
#====================================================================
# msgbox
#====================================================================
#@ solidify:lvh_msgbox,weak
class lvh_msgbox : lvh_obj
static var _lv_class = lv.msgbox
var _modal
# sub_objects
var _header, _footer, _content, _title
var _buttons # array containing the buttons, to apply styles later
#====================================================================
# init
#
# parent: LVGL parent object (used to create a sub-object)
# page: HASPmota page object
# jline: JSONL definition of the object from HASPmota template (used in sub-classes)
# obj: (opt) LVGL object if it already exists and was created prior to init()
# parent_lvh: HASPmota parent object defined by `parentid`
#====================================================================
def init(parent, page, jline, obj, parent_lvh)
self._buttons = []
self._modal = bool(jline.find("modal", false))
if (self._modal)
# the object created as modal is on top of everything
self._lv_obj = lv.msgbox(0)
end
super(self).init(parent, page, jline, self._lv_obj, parent_lvh)
# apply some default styles
self.text_align = 2 # can be overriden
self.bg_opa = 255 # can be overriden
end
#====================================================================
# register_event_cb
#
# Override the normal event handler, we are only interested
# in events on buttons
#====================================================================
def register_event_cb()
# nothing to register for now, event_cb is allocated when buttons are allocted in `setoptions`
end
# update after parsing
#
def post_config()
var lvh_class = self._page._hm.lvh_obj # get `lvh_obj` class from root instance
var _lv_obj = self._lv_obj # get msgbox lvgl object
# read the method, and return nil if exception 'value_error' occured
def get_obj_safe(method)
try
var lv_obj = method(_lv_obj) # equivalent of `self._lv_obj.get_XXX()` where XXX is header/footer/title/content
return lvh_class(nil, self._page, {}, lv_obj, self) # instanciate a local lvh object
except 'value_error'
return nil
end
end
super(self).post_config()
# get sub-objects
self._header = get_obj_safe(_lv_obj.get_header)
self._footer = get_obj_safe(_lv_obj.get_footer)
self._content = get_obj_safe(_lv_obj.get_content)
self._title = get_obj_safe(_lv_obj.get_title)
end
#- ------------------------------------------------------------#
# `setmember` virtual setter
#
# If the key starts with `footer_`, `header_`, `title_` or `content_`
# send to the corresponding object
#- ------------------------------------------------------------#
def setmember(k, v)
import string
if string.startswith(k, 'footer_') && self._footer
self._footer.setmember(k[7..], v)
elif string.startswith(k, 'header_') && self._header
self._header.setmember(k[7..], v)
elif string.startswith(k, 'title_') && self._title
self._title.setmember(k[6..], v)
elif string.startswith(k, 'content_') && self._content
self._content.setmember(k[8..], v)
elif string.startswith(k, 'buttons_') && self._buttons
for btn: self._buttons
btn.setmember(k[8..], v)
end
else
super(self).setmember(k, v)
end
end
def member(k)
import string
if string.startswith(k, 'footer_') && self._footer
return self._footer.member(k[7..])
elif string.startswith(k, 'header_') && self._header
return self._header.member(k[7..])
elif string.startswith(k, 'title_') && self._title
return self._title.member(k[6..])
elif string.startswith(k, 'content_') && self._content
return self._content.member(k[8..])
else
return super(self).member(k)
end
end
# private function to add a button, create the lvh class and register callbacks
def _add_button(msg)
var lvh_class = self._page._hm.lvh_obj # get `lvh_obj` class from root instance
var btn_lv = self._lv_obj.add_footer_button(msg)
var btn_lvh = lvh_class(nil, self._page, {}, btn_lv, self) # instanciate a local lvh object
self._buttons.push(btn_lvh)
end
def set_options(l)
if (isinstance(l, list) && size(l) > 0)
for msg: l
self._add_button(msg)
end
else
print("HTP: 'msgbox' needs 'options' to be a list of strings")
end
end
def get_options()
end
def set_title(t)
self._lv_obj.add_title(str(t))
end
def get_title()
# self._lv_obj.get_title()
end
def set_text(t)
self._lv_obj.add_text(str(t))
end
def get_text()
# self._lv_obj.get_text()
end
end
#====================================================================
# spinner
#====================================================================
#@ solidify:lvh_spinner,weak
class lvh_spinner : lvh_arc
static var _lv_class = lv.spinner
# static var _EVENTS = EVENTS_ALL # inherited
var _speed, _angle
#====================================================================
# init
#
# parent: LVGL parent object (used to create a sub-object)
# page: HASPmota page object
# jline: JSONL definition of the object from HASPmota template (used in sub-classes)
# obj: (opt) LVGL object if it already exists and was created prior to init()
# parent_lvh: HASPmota parent object defined by `parentid`
#====================================================================
def init(parent, page, jline, lv_instance, parent_obj)
var angle = jline.find("angle", 60)
var speed = jline.find("speed", 1000)
self._lv_obj = lv.spinner(parent)
self._lv_obj.set_anim_params(speed, angle)
super(self).init(parent, page, jline, self._lv_obj, parent_obj)
end
def set_angle(t) end
def get_angle() end
def set_speed(t) end
def get_speed() end
end
#====================================================================
# img
#====================================================================
#@ solidify:lvh_img,weak
class lvh_img : lvh_obj
static var _lv_class = lv.image
var _raw # used to store raw image in RAM
var _imd_dsc
def set_auto_size(v)
if v
self._lv_obj.set_inner_align(lv.IMAGE_ALIGN_STRETCH)
end
end
def get_auto_size() end
def set_angle(v)
v = int(v)
self._lv_obj.set_angle(v)
end
def get_angle()
return self._lv_obj.get_angle()
end
#- ------------------------------------------------------------#
# `src` virtual setter
# If source is `tasmota_logo`, use the embedded logo
#- ------------------------------------------------------------#
def set_src(t)
if (t == 'tasmota_logo')
self._lv_obj.set_tasmota_logo()
else
self._lv_obj.set_src(t)
end
end
#- ------------------------------------------------------------#
# `raw` virtual setter
# Decode base64
#- ------------------------------------------------------------#
def set_raw(t)
self._raw = bytes().fromb64(t)
var img_dsc = lv.lv_image_dsc()
img_dsc.header_cf = lv.COLOR_FORMAT_RAW
#img_dsc.header_w = 0
#img_dsc.header_h = 0
img_dsc.data_size = size(self._raw)
img_dsc.data = self._raw._buffer()
self._imd_dsc = img_dsc
self._lv_obj.set_src(img_dsc)
end
end
#====================================================================
# qrcode
#====================================================================
#@ solidify:lvh_qrcode,weak
class lvh_qrcode : lvh_obj
static var _lv_class = lv.qrcode
var qr_text # any change needs the text to be update again
def post_config()
super(self).post_config()
self._update()
end
def _update()
var t = self.qr_text
if (t != nil)
self._lv_obj.update(t, size(t))
end
end
# ignore attributes, spinner can't be changed once created
def set_qr_size(t) self._lv_obj.set_size(t) self._update() end
def set_size(t) self._lv_obj.set_size(t) self._update() end
def get_qr_size() end
def get_size() end
def set_qr_dark_color(t) self._lv_obj.set_dark_color(self.parse_color(t)) self._update() end
def set_dark_color(t) self._lv_obj.set_dark_color(self.parse_color(t)) self._update() end
def get_qr_dark_color() end
def get_dark_color() end
def set_qr_light_color(t) self._lv_obj.set_light_color(self.parse_color(t)) self._update() end
def set_light_color(t) self._lv_obj.set_light_color(self.parse_color(t)) self._update() end
def get_qr_light_color() end
def get_light_color() end
def set_qr_text(t)
self.qr_text = str(t)
self._update()
end
def get_qr_text() end
end
#====================================================================
# slider
#====================================================================
#@ solidify:lvh_slider,weak
class lvh_slider : lvh_obj
static var _lv_class = lv.slider
# static var _EVENTS = EVENTS_ALL
def set_val(t)
self._val = t
self._lv_obj.set_value(t, 0) # add second parameter - no animation
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
end
#====================================================================
# roller
#====================================================================
#@ solidify:lvh_roller,weak
class lvh_roller : lvh_obj
static var _lv_class = lv.roller
def set_val(t)
self._val = t
self._lv_obj.set_selected(t, 0) # add second parameter - no animation
end
def get_val()
return self._lv_obj.get_selected()
end
def set_options(t)
self._lv_obj.set_options(t, lv.ROLLER_MODE_NORMAL)
end
def get_options()
return self._lv_obj.get_options()
end
def set_text(t)
raise "attribute_error", "set_text unsupported on roller"
end
def get_text()
# allocate a bytes buffer
var b = bytes().resize(256) # force 256 bytes
self._lv_obj.get_selected_str(b._buffer(), 256)
b = self.remove_trailing_zeroes(b)
return b.asstring()
end
end
#====================================================================
# led
#====================================================================
#@ solidify:lvh_led,weak
class lvh_led : lvh_obj
static var _lv_class = lv.led
# `val` is a synonym for `brightness`
def set_val(t)
self._val = t
self._lv_obj.set_brightness(t)
end
def get_val()
return self._lv_obj.get_brightness()
end
def set_color(t)
var v = self.parse_color(t)
self._lv_obj.set_color(v)
end
def get_color()
end
end
#====================================================================
# dropdown
#====================================================================
#@ solidify:lvh_dropdown,weak
class lvh_dropdown : lvh_obj
static var _lv_class = lv.dropdown
# static var _EVENTS = EVENTS_ALL
var _symbol # we need to keep a reference to the string used for symbol to avoid GC
static var _dir = [ lv.DIR_BOTTOM, lv.DIR_TOP, lv.DIR_LEFT, lv.DIR_RIGHT ] # 0 = down, 1 = up, 2 = left, 3 = right
def set_val(t)
self._val = t
self._lv_obj.set_selected(t)
end
def get_val()
return self._lv_obj.get_selected()
end
def set_text(t)
# set_text sets a static text displayed whatever the value
# use `nil` to set back the text of the selected value
self._lv_obj.set_text(t)
end
def get_text()
var static_text = self._lv_obj.get_text()
if static_text == nil
# allocate a bytes buffer
var b = bytes().resize(256) # force 256 bytes
self._lv_obj.get_selected_str(b._buffer(), 256)
b = self.remove_trailing_zeroes(b)
return b.asstring()
else
return static_text
end
end
# direction needs a conversion from HASPmota numbers and LVGL's
def set_direction(t)
t = int(t)
# 0 = down, 1 = up, 2 = left, 3 = right
if (t < 0) || (t > 3) t = 0 end
self._lv_obj.set_dir(self._dir[t])
if t == 1 self._symbol = lv.SYMBOL_UP
elif t == 2 self._symbol = lv.SYMBOL_LEFT
elif t == 3 self._symbol = lv.SYMBOL_RIGHT
else self._symbol = lv.SYMBOL_DOWN
end
self._lv_obj.set_symbol(self._symbol)
end
def get_direction()
var dir = self._lv_obj.get_dir()
var i = 0
while i < size(self._dir)
if dir == self._dir[i] return i end
i += 1
end
return -1
end
# show_selected (bool) is a HASPmota addition
# only meaningful if set to `true`, setting to false requires a call to `set_text`
def set_show_selected(t)
if t
self._lv_obj.set_text(nil) # undo static text
end
end
def get_show_selected()
var static_text = self._lv_obj.get_text()
return (static_text == nil)
end
end
#====================================================================
# dropdown_list (accessing the list object)
#====================================================================
#@ solidify:lvh_dropdown_list,weak
class lvh_dropdown_list : lvh_obj
static var _lv_class = nil
# static var _EVENTS = EVENTS_NONE
def post_init()
self._lv_obj = nil # default to nil object, whatever it was initialized with
# check if it is the parent is a spangroup
if isinstance(self._parent_lvh, self._page._hm.lvh_dropdown)
self._lv_obj = lv.list(self._parent_lvh._lv_obj.get_list()._p)
else
print("HSP: 'dropdown_list' should have a parent of type 'dropdown'")
end
super(self).post_init()
end
end
#====================================================================
# bar
#====================================================================
#@ solidify:lvh_bar,weak
class lvh_bar : lvh_obj
static var _lv_class = lv.bar
def post_init()
super(self).post_init()
if isinstance(self._parent_lvh, self._page._hm.lvh_scale)
# if sub-object of scale, copy min and max
var min = self._parent_lvh._lv_obj.get_range_min_value()
var max = self._parent_lvh._lv_obj.get_range_max_value()
self._lv_obj.set_range(min, max)
end
end
def set_val(t)
self._val = t
self._lv_obj.set_value(t, lv.ANIM_OFF)
end
def set_min(t)
self._lv_obj.set_range(int(t), self._lv_obj.get_max_value())
end
def set_max(t)
self._lv_obj.set_range(self._lv_obj.get_min_value(), 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
end
#====================================================================
# line
#====================================================================
#@ solidify:lvh_line,weak
class lvh_line : lvh_obj
static var _lv_class = lv.line
var _lv_points # needs to save to avoid garbage collection
# list of points
def set_points(t)
if isinstance(t, list)
var pts = []
for p: t
if (isinstance(p, list) && size(p) == 2)
var pt = lv.point()
pt.x = int(p[0])
pt.y = int(p[1])
pts.push(pt)
end
end
var pt_arr = lv.point_arr(pts)
self._lv_points = pt_arr
self._lv_obj.set_points_mutable(pt_arr, size(pts))
else
print(f"HSP: 'line' wrong format for 'points' {t}")
end
end
end
#====================================================================
# scale
#====================================================================
#@ solidify:lvh_scale,weak
class lvh_scale : lvh_obj
static var _lv_class = lv.scale
var _options # need to keep the reference alive to avoid GC
var _options_arr # need to keep the reference alive to avoid GC
def set_text_src(l)
if (isinstance(l, list) && size(l) > 0)
# check if the last element is empty, if not add an empty string
if size(l[-1]) > 0
l.push("")
end
self._options = l
self._options_arr = lv.str_arr(l)
self._lv_obj.set_text_src(self._options_arr)
else
print("HTP: 'scale' needs 'text_src' to be a list of strings")
end
end
def get_text_src()
return self._options
end
def set_min(t)
self._lv_obj.set_range(int(t), self._lv_obj.get_range_max_value())
end
def set_max(t)
self._lv_obj.set_range(self._lv_obj.get_range_min_value(), int(t))
end
def get_min()
return self._lv_obj.get_range_min_value()
end
def get_max()
return self._lv_obj.get_range_max_value()
end
end
#====================================================================
# scale_section
#====================================================================
#@ solidify:lvh_scale_section,weak
class lvh_scale_section : lvh_root
static var _lv_class = nil
var _style # style object
var _style10 # style for LV_PART_INDICATOR
var _style30 # style for LV_PART_ITEMS
var _min, _max
def post_init()
self._lv_obj = nil # default to nil object, whatever it was initialized with
self._min = 0 # default value by LVGL
self._max = 0 # default value by LVGL
# check if it is the parent is a spangroup
if isinstance(self._parent_lvh, self._page._hm.lvh_scale)
# print(">>> GOOD")
self._lv_obj = self._parent_lvh._lv_obj.add_section()
self._style = lv.style() # we create a specific lv.style object for this object
self._lv_obj.set_style(lv.PART_MAIN, self._style)
self._style10 = lv.style() # we create a specific lv.style object for this object
self._lv_obj.set_style(lv.PART_INDICATOR, self._style10)
self._style30 = lv.style() # we create a specific lv.style object for this object
self._lv_obj.set_style(lv.PART_ITEMS, self._style30)
else
print("HSP: 'scale_section' should have a parent of type 'scale'")
end
# super(self).post_init() # call super - not needed for lvh_root
end
def set_min(t)
var min = int(t)
var max = self._max
if (max < min) max = min end
self.set_range(min, max)
end
def set_max(t)
var min = self._min
var max = int(t)
if (min > max) min = max end
self.set_range(min, max)
end
def set_range(min, max)
self._min = min
self._max = max
self._lv_obj.set_range(min, max)
end
#====================================================================
# `_delete` special attribute used to delete the object
#====================================================================
# the actual _delete method, overriden
def _delete()
self._style.del()
self._style = nil
self._style10.del()
self._style10 = nil
self._style30.del()
self._style30 = nil
super(self)._delete()
end
#- ------------------------------------------------------------#
# `setmember` virtual setter
# trimmed down version for style only
#- ------------------------------------------------------------#
def setmember(k, v)
import string
import introspect
if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# parse value in percentage
if string.endswith(k, "%")
k = k[0..-2]
v = lv.pct(int(v))
end
# check if the attribute ends with 2 digits, if so remove the two suffix digits
var suffix_digits = nil
if size(k) >= 3
var char_last_1 = string.byte(k[-1])
var char_last_2 = string.byte(k[-2])
if (char_last_1 >= 0x30 && char_last_1 <= 0x39 && char_last_2 >= 0x30 && char_last_2 <= 0x39)
# we extract the last 2 digits
suffix_digits = int(k[-2..])
k = k[0..-3] # remove 2 last digits
# only style modifiers allowed are `10` and `30`
if suffix_digits != 10 && suffix_digits != 30
raise "value_error", "only modifiers '10' or '30' allowed"
end
end
end
# if attribute name is in ignore list, abort
if self._attr_ignore.find(k) != nil return end
# select the right style object
var style = self._style
if suffix_digits == 10
style = self._style10
elif suffix_digits == 30
style = self._style30
end
# first check if there is a method named `set_X()`
var f = introspect.get(self, "set_" + k)
if type(f) == 'function'
# print(f">>>: setmember local method set_{k}")
f(self, v)
return
end
# simply check if a method `set_{k}` exists
f = introspect.get(style, "set_" + k) # look at style
# print(f">>>: span name={'set_' + k} {f=}")
if (type(f) == 'function')
# if the attribute contains 'color', convert to lv_color
if self.is_color_attribute(k)
v = self.parse_color(v)
end
# invoke
try
f(style, v)
except .. as e, m
raise e, m + " for " + k
end
return nil
else
print("HSP: Could not find function set_" + k)
end
end
end
#====================================================================
# scale_line
#====================================================================
#@ solidify:lvh_scale_line,weak
class lvh_scale_line : lvh_line
var _needle_length
# var _lv_points # in superclass
def post_init()
# check if it is the parent is a spangroup
if !isinstance(self._parent_lvh, self._page._hm.lvh_scale)
print("HSP: 'scale_line' should have a parent of type 'scale'")
end
self._needle_length = 0
self._lv_points = lv.point_arr([lv.point(), lv.point()]) # create an array with 2 points
super(self).post_init()
end
def set_needle_length(t)
self._needle_length = int(t)
# force a refresh
if self._val != nil
self.set_val(self._val)
end
end
def get_needle_length()
return self._needle_length
end
def set_val(t)
super(self).set_val(t)
self._parent_lvh._lv_obj.set_line_needle_value(self._lv_obj, self._needle_length, self._val)
# work-around for points being global static
if (self._lv_obj.get_point_count() == 2) # check that there are only 2 points
# read back the computed points
var p_arr = bytes(self._lv_obj.get_points(), size(self._lv_points))
self._lv_points.setbytes(0, p_arr)
self._lv_obj.set_points_mutable(self._lv_points, 2)
end
end
end
#====================================================================
# spangroup
#====================================================================
#@ solidify:lvh_spangroup,weak
class lvh_spangroup : lvh_obj
static var _lv_class = lv.spangroup
# label do not need a sub-label
def post_init()
self._lv_obj.set_mode(lv.SPAN_MODE_BREAK) # use lv.SPAN_MODE_BREAK by default
self._lv_obj.refr_mode()
super(self).post_init() # call super -- not needed
end
# refresh mode
def refr_mode()
self._lv_obj.refr_mode()
end
end
#====================================================================
# span
#====================================================================
#@ solidify:lvh_span,weak
class lvh_span : lvh_root
static var _lv_class = nil
# label do not need a sub-label
var _style # style object
def post_init()
self._lv_obj = nil # default to nil object, whatever it was initialized with
# check if it is the parent is a spangroup
if isinstance(self._parent_lvh, self._page._hm.lvh_spangroup)
# print(">>> GOOD")
self._lv_obj = self._parent_lvh._lv_obj.new_span()
self._style = self._lv_obj.get_style()
else
print("HSP: 'span' should have a parent of type 'spangroup'")
end
# super(self).post_init() # call super - not needed for lvh_root
end
#====================================================================
def set_text(t)
self._lv_obj.set_text(str(t))
end
#====================================================================
def set_text_font(t)
var font = self.parse_font(t)
if font != nil
self._style.set_text_font(font)
self._parent_lvh.refr_mode()
end
end
#- ------------------------------------------------------------#
# Internal utility functions
#
# Mapping of virtual attributes
#
#- ------------------------------------------------------------#
# There are no attributes that can be read from `lv.style``
# so we don't need virtual members
#- ------------------------------------------------------------#
# def member(k)
# import string
# import introspect
# if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# # if attribute name is in ignore list, abort
# if self._attr_ignore.find(k) != nil return end
# # first check if there is a method named `get_X()`
# var f = introspect.get(self, "get_" + k)
# if type(f) == 'function'
# # print(f">>>: setmember local method set_{k}")
# return f(self)
# end
# # finally try any `get_XXX` within the LVGL object
# f = introspect.get(self._style, "get_" + k)
# if type(f) == 'function' # found and function, call it
# return f(self._style)
# end
# # fallback to exception if attribute unknown or not a function
# return module("undefined")
# end
#- ------------------------------------------------------------#
# `setmember` virtual setter
# trimmed down version for style only
#- ------------------------------------------------------------#
def setmember(k, v)
import string
import introspect
if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# parse value in percentage
if string.endswith(k, "%")
k = k[0..-2]
v = lv.pct(int(v))
end
# if attribute name is in ignore list, abort
if self._attr_ignore.find(k) != nil return end
# first check if there is a method named `set_X()`
var f = introspect.get(self, "set_" + k)
if type(f) == 'function'
# print(f">>>: setmember local method set_{k}")
f(self, v)
return
end
# simply check if a method `set_{k}` exists
f = introspect.get(self._style, "set_" + k) # look at style
# print(f">>>: span name={'set_' + k} {f=}")
if (type(f) == 'function')
# if the attribute contains 'color', convert to lv_color
if self.is_color_attribute(k)
v = self.parse_color(v)
end
# invoke
try
f(self._style, v)
self._parent_lvh.refr_mode()
except .. as e, m
raise e, m + " for " + k
end
return nil
else
print("HSP: Could not find function set_" + k)
end
end
end
#====================================================================
# tabview
#====================================================================
#@ solidify:lvh_tabview,weak
class lvh_tabview : lvh_obj
static var _lv_class = lv.tabview
var _tab_list # list of tabs
# label do not need a sub-label
def post_init()
self._tab_list = []
super(self).post_init() # call super -- not needed
end
#====================================================================
# direction for buttons
#====================================================================
static var _direction = [
lv.DIR_NONE, # 0 = none
lv.DIR_TOP, # 1 = top
lv.DIR_BOTTOM, # 2 = bottom
lv.DIR_LEFT, # 3 = left
lv.DIR_RIGHT, # 4 = right
]
def set_btn_pos(v)
v = int(v)
if (v == nil) || (v < 0) || (v >= size(self._direction))
v = 0
end
var direction = self._direction[v]
self._lv_obj.set_tab_bar_position(direction)
end
#====================================================================
# management of `_tab_list` list
#====================================================================
# add lvh_tab instance as they are created
def push_tab(t)
self._tab_list.push(t)
end
# returns the index of the tab instance, or `nil` if not found
def find_tab(t)
return self._tab_list.find(t)
end
#====================================================================
# count read-only attribute, returns number of tabs
#====================================================================
def get_count()
return self._lv_obj.get_tab_count()
end
def get_val()
return self._lv_obj.get_tab_active()
end
# change current tab
# v: value of new tab
# stop: (opt) if true, don't defer again to avoid infinite loop
def set_val(v, stop)
var v_max = self.get_count()
if (v_max == 0)
# probably not constructed yet
if (!stop)
tasmota.set_timer(0, def () self.set_val(v, true #-stop propagation-#) end)
end
else
if (v == nil) v = 0 end
if (v < 0) v = 0 end
if (v >= v_max) v = v_max - 1 end
self._lv_obj.set_active(v, lv.ANIM_OFF)
end
end
def get_text()
var val = self.get_val()
if (val >= 0) && (val < self.get_count())
return self._tab_list[val].get_text()
else
return nil
end
end
end
#====================================================================
# tab
#====================================================================
#@ solidify:lvh_tab.lvh_btn_tab,weak
#@ solidify:lvh_tab,weak
class lvh_tab : lvh_obj
static var _lv_class = nil
# label do not need a sub-label
var _text # text label of the tab
var _btn # btn lvh object
static class lvh_btn_tab : lvh_obj
static var _lv_class = lv.button
#====================================================================
# specific post-init wihtout events
#====================================================================
def post_init()
self._lv_obj.set_style_radius(0, 0) # set default radius to `0` for rectangle tabs
# self.register_event_cb()
end
end
#====================================================================
# init
#
# parent: LVGL parent object (used to create a sub-object)
# page: HASPmota page object
# jline: JSONL definition of the object from HASPmota template (used in sub-classes)
# obj: (opt) LVGL object if it already exists and was created prior to init()
# parent_lvh: HASPmota parent object defined by `parentid`
#====================================================================
def init(parent, page, jline, lv_instance, parent_obj)
self.set_text(jline.find("text")) # anticipate reading 'text' for creation
super(self).init(parent, page, jline, lv_instance, parent_obj)
end
def post_init()
self._lv_obj = nil # default to nil object, whatever it was initialized with
# check if it is the parent is a spangroup
if isinstance(self._parent_lvh, self._page._hm.lvh_tabview)
if (self._text != nil)
self._lv_obj = self._parent_lvh._lv_obj.add_tab(self._text)
# get the last button object of the tab bar and create an instance of simplified btn
var tab_bar = self._parent_lvh._lv_obj.get_tab_bar()
var btn_class = lv.obj_class(lv.button._class)
var btn_count = tab_bar.get_child_count_by_type(btn_class)
var btn_obj = tab_bar.get_child_by_type(btn_count - 1, btn_class) # get last button
self._btn = self.lvh_btn_tab(nil, self._page, {}, btn_obj, self) # instanciate a local lvh object
# add to parent list
self._parent_lvh.push_tab(self)
else
print("HSP: 'tab' requires 'text' attribute")
end
else
print("HSP: 'tab' should have a parent of type 'tabview'")
end
# super(self).post_init() # call super - not needed for lvh_root
end
#====================================================================
def set_text(t)
self._text = str(t)
end
def get_text()
return self._text
end
#- ------------------------------------------------------------#
# `setmember` virtual setter
#
# If the key starts with `bar_`
# send to the corresponding object
#- ------------------------------------------------------------#
def setmember(k, v)
import string
if string.startswith(k, 'tab_')
self._btn.setmember(k[4..], v)
else
super(self).setmember(k, v)
end
end
def member(k)
import string
if string.startswith(k, 'tab_')
return self._btn.member(k[4..])
else
return super(self).member(k)
end
end
end
#################################################################################
# Special case for lv.chart
# Adapted to getting values one after the other
#################################################################################
#@ solidify:lvh_chart,weak
class lvh_chart : lvh_obj
static var _lv_class = lv.chart
# ser1/ser2 contains the first/second series of data
var _ser1, _ser2
# y_min/y_max contain the main range for y. Since LVGL does not have getters, we need to memorize on our side the lates tvalues
var _y_min, _y_max
# h_div/v_div contain the horizontal and vertical divisions, we need to memorize values because both are set from same API
var _h_div, _v_div
def post_init()
# default values from LVGL are 0..100
self._y_min = 0
self._y_max = 100
# default values
#define LV_CHART_HDIV_DEF 3
#define LV_CHART_VDIV_DEF 5
self._h_div = 3
self._v_div = 5
self._lv_obj.set_update_mode(lv.CHART_UPDATE_MODE_SHIFT)
self._ser1 = self._lv_obj.add_series(lv.color(0xEE4444), lv.CHART_AXIS_PRIMARY_Y)
self._ser2 = self._lv_obj.add_series(lv.color(0x44EE44), lv.CHART_AXIS_PRIMARY_Y)
end
def add_point(v)
self._lv_obj.set_next_value(self._ser1, v)
end
def add_point2(v)
self._lv_obj.set_next_value(self._ser2, v)
end
def set_val(v)
self._val = v
self.add_point(v)
end
def set_val2(v)
self.add_point2(v)
end
def get_y_min()
return self._y_min
end
def get_y_max()
return self._y_max
end
def set_y_min(_y_min)
self._y_min = _y_min
self._lv_obj.set_range(lv.CHART_AXIS_PRIMARY_Y, self._y_min, self._y_max)
end
def set_y_max(_y_max)
self._y_max = _y_max
self._lv_obj.set_range(lv.CHART_AXIS_PRIMARY_Y, self._y_min, self._y_max)
end
def set_series1_color(color)
self._lv_obj.set_series_color(self._ser1, self.parse_color(color))
end
def set_series2_color(color)
self._lv_obj.set_series_color(self._ser2, self.parse_color(color))
end
def set_h_div_line_count(_h_div)
self._h_div = _h_div
self._lv_obj.set_div_line_count(self._h_div, self._v_div)
end
def set_v_div_line_count(_v_div)
self._v_div = _v_div
self._lv_obj.set_div_line_count(self._h_div, self._v_div)
end
end
#====================================================================
# btnmatrix
#====================================================================
#@ solidify:lvh_btnmatrix,weak
class lvh_btnmatrix : lvh_obj
static var _lv_class = lv.buttonmatrix
var _options # need to keep the reference alive to avoid GC
var _options_arr # need to keep the reference alive to avoid GC
def set_options(l)
if (isinstance(l, list) && size(l) > 0)
# check if the last element is empty, if not add an empty string
if size(l[-1]) > 0
l.push("")
end
self._options = l
self._options_arr = lv.str_arr(l)
self._lv_obj.set_map(self._options_arr)
else
print("HTP: 'btnmatrix' needs 'options' to be a list of strings")
end
end
def get_options()
return self._options
end
end
#====================================================================
# cpicker - color picker
#
# OpenHASP maps to LVGL 7 `cpicker`
# However `cpicker` was replaced with `colorwheel` in LVGL 8
# and removed in LVGL 9.
# We have ported back `colorwheel` from LVGL 8 to LVGL 9
#====================================================================
#@ solidify:lvh_cpicker,weak
class lvh_cpicker : lvh_obj
static var _lv_class = lv.colorwheel
static var _CW_MODES = ['hue', 'saturation', 'value']
# we need a non-standard initialization of lvgl object
def init(parent, page, jline, obj, parent_lvh)
obj = lv.colorwheel(parent, true #-knob_recolor = true-#)
super(self).init(parent, page, jline, obj, parent_lvh)
self.set_scale_width(25) # align to OpenHASP default value
end
def set_color(t)
var v = self.parse_color(t)
self._lv_obj.set_rgb(v)
end
def get_color()
var color = self._lv_obj.get_rgb()
return f"#{color:06X}"
end
def set_mode(s)
var mode = self._CW_MODES.find(s)
if (mode != nil)
self._lv_obj.set_mode(mode)
else
raise "value_error", f"unknown color mode '{mode}'"
end
end
def get_mode()
var mode = self._lv_obj.get_color_mode()
if (mode >= 0) && (mode < size(self._CW_MODES))
return self._CW_MODES[mode]
else
return 'unknown'
end
end
def set_mode_fixed(b)
b = bool(b)
self._lv_obj.set_mode_fixed(b)
end
def get_mode_fixed()
return self._lv_obj.get_color_mode_fixed()
end
def set_scale_width(v)
self._lv_obj.set_style_arc_width(int(v), 0)
end
def get_scale_width()
return self._lv_obj.get_style_arc_width(0)
end
# pad_inner is ignored (for now?)
def set_pad_inner() end
def get_pad_inner() end
end
#################################################################################
#
# All other subclasses than just map the LVGL object
# and doesn't have any specific behavior
#
#################################################################################
#@ solidify:lvh_btn,weak
class lvh_btn : lvh_obj static var _lv_class = lv.button end
#@ solidify:lvh_checkbox,weak
class lvh_checkbox : lvh_obj static var _lv_class = lv.checkbox end
# class lvh_textarea : lvh_obj static var _lv_class = lv.textarea end
# special case for scr (which is actually lv_obj)
#@ solidify:lvh_scr,weak
class lvh_scr : lvh_obj
static var _lv_class = nil # no class for screen
end
#################################################################################
# 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)
#@ solidify:lvh_page,weak
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 _hm # HASPmota global object
# haspmota attributes for page are on item `#0`
var prev, next, back # (int) id values for `prev`, `next`, `back` buttons
#====================================================================
# `init`
#
# arg1: `page_number` (int) HASPmota page id
# defaults to `1` if not specified
# page 0 is special, visible on all pages. Internally uses `layer_top`
# arg2: `hm` global HASPmota monad object
# page_number: haspmota page number, defaults to `1` if not specified
#====================================================================
def init(page_number, hm)
import global
self._hm = hm # memorize HASPmota 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 == 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 lvh_scr_class = self._hm.lvh_scr
var obj_scr = lvh_scr_class(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<page_number>, ex `p1`
# create a global for the page attributes as p<page_number>b0, ex `p1b0`
global.(f"p{self._page_id}") = self
global.(f"p{self._page_id}b0") = obj_scr
end
#####################################################################
# General Setters and Getters
#####################################################################
#- ------------------------------------------------------------#
# Internal utility functions
#
# Mapping of virtual attributes
#
#- ------------------------------------------------------------#
# `member` virtual getter
#- ------------------------------------------------------------#
def member(k)
import string
import introspect
if string.startswith(k, "set_") || string.startswith(k, "get_") return end
# if attribute name is in ignore list, abort
# if self._attr_ignore.find(k) != nil return end
# we don't need an ignore list for pages
# first check if there is a method named `get_X()`
var f = introspect.get(self, "get_" + k) # call self method
if type(f) == 'function'
# print(f">>>: getmember local method get_{k}")
return f(self)
end
# fallback to exception if attribute unknown or not a function
return module("undefined")
end
#====================================================================
# 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 get_obj(obj_id)
return self._obj_id.find(obj_id)
end
def add_obj(obj_id, obj_lvh)
# add object to page object
self._obj_id[obj_id] = obj_lvh
# create a global variable for this object of form p<page>b<id>, ex p1b2
var glob_name = format("p%ib%i", obj_lvh._page.id(), obj_id)
global.(glob_name) = obj_lvh
end
def remove_obj(obj_id)
# remove object from page object
var obj_lvh = self._obj_id.find(obj_id)
self._obj_id.remove(obj_id)
# set the global variable to `nil`
if obj_lvh
var glob_name = format("p%ib%i", obj_lvh._page.id(), obj_id)
global.(glob_name) = nil
end
end
#====================================================================
# `delete` special attribute used to delete the object
#====================================================================
def get_clear()
self._clear()
return def () end
end
def _clear()
# iterate on all objects and try to delete
# we first get a copy of all ids so we can delete and continue iterating
# without fearing about an infinite loop
var ids = []
for id: self._obj_id.keys()
ids.push(id)
end
# we iterate until the array is empty
var idx = 0
while idx < size(ids)
var page_id = ids[idx]
if (page_id != 0) && self._obj_id.contains(page_id)
# first check if the id is still in the page - it could have been already removed if it's a sub-object
self._obj_id[page_id]._delete()
end
idx += 1
end
self._obj_id = {} # clear map
end
def get_delete()
self._delete()
return def () end
end
def _delete()
# remove from page, also change page if this is the current one
self._hm._remove_page(self._page_id)
# clear content
self._clear()
end
#====================================================================
# `show` transition from one page to another
# duration: in ms, default 500 ms
# anim: -1 right to left, 1 left to right (default), `nil` auto, 0 none
#
# 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._hm.page_dir_to(self.id())
end
# send page events
var event_str_in = format('{"hasp":{"p%i":"out"}}', self._hm.lvh_page_cur_idx)
tasmota.set_timer(0, /-> tasmota.publish_rule(event_str_in))
var event_str_out = format('{"hasp":{"p%i":"in"}}', self._page_id)
tasmota.set_timer(0, /-> tasmota.publish_rule(event_str_out))
# change current page
self._hm.lvh_page_cur_idx = self._page_id
if (anim == 0)
lv.screen_load(self._lv_scr)
else # animation
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.screen_load_anim(self._lv_scr, anim_lvgl, duration, 0, false)
end
end
end
#################################################################################
#
# class `HASPmota` to initialize the HASPmota parsing
#
#################################################################################
# main class controller, meant to be a singleton and the only externally used class
class HASPmota
var dark # (bool) use dark theme?
var hres, vres # (int) resolution
var scr # (lv_obj) default LVGL screen
var r16 # (lv_font) robotocondensed fonts size 16
# haspmota objects
var lvh_pages # (list of lvg_page) list of pages
var lvh_page_cur_idx # (int) current page index number
var lvh_page_cur_idx_parsing # (int) index of the current page related to parsing JSONL, can be different from the displayed page
# regex patterns
var re_page_target # compiled regex for action `p<number>`
# specific event_cb handling for less memory usage since we are registering a lot of callbacks
var event # try to keep the event object around and reuse it
var event_cb # the low-level callback for the closure to be registered
# assign lvh_page to a static attribute
static lvh_root = lvh_root
static lvh_obj = lvh_obj
static lvh_fixed = lvh_fixed
static lvh_flex = lvh_flex
static lvh_page = lvh_page
static lvh_scr = lvh_scr
# 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_line = lvh_line
static lvh_img = lvh_img
static lvh_dropdown = lvh_dropdown
static lvh_dropdown_list = lvh_dropdown_list
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_cpicker = lvh_cpicker
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 lvh_led = lvh_led
static lvh_scale = lvh_scale
static lvh_scale_section = lvh_scale_section
static lvh_scale_line = lvh_scale_line
static lvh_spangroup = lvh_spangroup
static lvh_span = lvh_span
static lvh_tabview = lvh_tabview
static lvh_tab = lvh_tab
static lvh_qrcode = lvh_qrcode
# special cases
static lvh_chart = lvh_chart
static def_templ_name = "pages.jsonl" # default template name
def init()
self.fix_lv_version()
import re
self.re_page_target = re.compile("p\\d+")
# nothing to put here up to now
end
# make sure that `lv.version` returns a version number
static def fix_lv_version()
import introspect
var v = introspect.get(lv, "version")
# if `lv.version` does not exist, v is `module('undefined')`
if type(v) != 'int' lv.version = 8 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
try
self.r16 = lv.font_embedded("robotocondensed", 16) # TODO what if does not exist
except ..
self.r16 = lv.font_embedded("montserrat", 14) # TODO what if does not exist
end
# set the theme for HASPmota
var th2 = lv.theme_haspmota_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
#====================================================================
# return an array of all pages numbers
#====================================================================
def get_pages()
return self.pages_list_sorted(nil)
end
#====================================================================
# return the current page being parsed with JSONL as `lvh_page` object
#====================================================================
def get_page_cur_parsing()
return self.lvh_pages[self.lvh_page_cur_idx_parsing]
end
#====================================================================
# load JSONL template
#====================================================================
def _load(templ_name)
import string
import json
var f = open(templ_name,"r")
var f_content = f.read()
f.close()
var jsonl = string.split(f_content, "\n")
f = nil # allow deallocation
f_content = nil
# parse each line
while size(jsonl) > 0
var jline = json.load(jsonl[0])
if type(jline) == 'instance'
if tasmota.loglevel(4)
tasmota.log(f"HSP: parsing line '{jsonl[0]}'", 4)
end
self.parse_page(jline) # parse page first to create any page related objects, may change self.lvh_page_cur_idx_parsing
# objects are created in the current page
if (self.lvh_pages == nil)
raise "value_error", "no page 'id' defined"
end
self.parse_obj(jline, self.lvh_pages[self.lvh_page_cur_idx_parsing]) # then parse object within this page
else
# check if it's invalid json
if size(string.tr(jsonl[0], " \t", "")) > 0
tasmota.log(f"HSP: invalid JSON line '{jsonl[0]}'", 2)
end
end
jline = nil
jsonl.remove(0)
end
jsonl = nil # make all of it freeable
# current page is always 1 when we start
var pages_sorted = self.pages_list_sorted(nil) # nil for full list
if (size(pages_sorted) == 0)
raise "value_error", "no page object defined"
end
self.lvh_page_cur_idx = pages_sorted[0]
self.lvh_pages[self.lvh_page_cur_idx].show(0, 0) # show first page
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_parsing
# 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 pages 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_object, event_code)
if event_code != lv.EVENT_CLICKED return end
self.page_show(lvh_object._action)
end
#====================================================================
# Execute a page changing action from string `action`
#
# Arg1 `action` can be `prev`, `next`, `back` or `p<number>`
# of `delete` if we are deleting the current page
# duration: in ms, default 500 ms
# anim: -1 right to left, 1 left to right (default), `nil` auto, 0 none
# Returns: the target page object if changed, or `nil` if still on same page
#====================================================================
def page_show(action, anim, duration)
# resolve between page numbers
# p1 is either a number or nil (stored value)
# p2 is the default value
# l is the list of page ids
def to_page_resolve(p1, p_def, l)
if (p1 != nil) && (l.find(p1) != nil)
return p1
else
return p_def
end
end
# action can be `prev`, `next`, `back`, or `p<number>` like `p1`
var to_page = nil
var cur_page = self.get_page_cur()
var sorted_pages_list = self.pages_list_sorted(self.lvh_page_cur_idx)
if size(sorted_pages_list) <= 1 # if only 1 page, do nothing
return nil
end
# 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 = to_page_resolve(int(cur_page.prev), sorted_pages_list[-1], sorted_pages_list)
elif action == 'next'
to_page = to_page_resolve(int(cur_page.next), sorted_pages_list[1], sorted_pages_list)
elif action == 'back'
to_page = to_page_resolve(int(cur_page.back), self.pages_list_sorted(nil)[0], sorted_pages_list)
elif action == 'delete'
to_page = to_page_resolve(int(cur_page.back), self.pages_list_sorted(nil)[0], sorted_pages_list)
if (to_page == cur_page.id())
to_page = to_page_resolve(int(cur_page.next), sorted_pages_list[1], sorted_pages_list)
end
elif self.re_page_target.match(action)
# action is supposed to be `p<number>` format
to_page = to_page_resolve(int(action[1..-1]), nil #-default to nil-#, sorted_pages_list)
end
# print(f"{action=} {to_page=}")
if (to_page != nil) && (to_page > 0) # we have a target
var to_page_obj = self.lvh_pages[to_page]
# print(f"{to_page_obj.id()=}")
if (to_page_obj != nil)
to_page_obj.show(anim, duration)
end
return to_page_obj
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"])
# print(f">>> parsing page {page}")
self.lvh_page_cur_idx_parsing = page # change current page
if (self.lvh_page_cur_idx == nil) # also set current page if we haven't any yet
self.lvh_page_cur_idx = page
end
# 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_parsing()
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
#====================================================================
# Remove page by id
#
# Should not be called directly. Indirectly called by `p<x>.delete`
#
# Only removes reference to the page at root level
# Change the active page if needed
#====================================================================
def _remove_page(page_id)
# check if we remove the active page
var cur_page_id = self.get_page_cur().id()
if (page_id == cur_page_id)
# if we try to delete the current page, move do main page
var to_page_obj = self.page_show("delete", 0, 0 #-no animation-#) # get the target page as result
if (to_page_obj == nil) # we didn't change page
return
end
end
# also update lvh_page_cur_idx_parsing, if we removed the current parsing page
if (self.lvh_page_cur_idx_parsing == page_id)
self.lvh_page_cur_idx_parsing = self.lvh_page_cur_idx
end
# remove object from page object
if self.lvh_pages.contains(page_id)
self.lvh_pages.remove(page_id)
end
# remove global for page
global.(f"p{page_id}") = nil
end
#====================================================================
# Event CB handling
#====================================================================
def register_event(lvh_obj, event_type)
import cb
import introspect
# create the callback to the closure only once
if self.event_cb == nil
self.event_cb = cb.gen_cb(/ event_ptr_i -> self.event_dispatch(event_ptr_i))
end
# register the C callback
var lv_obj = lvh_obj._lv_obj
# we pass the cb as a comptr so it's already a C pointer
lv_obj.add_event_cb(self.event_cb, event_type, introspect.toptr(lvh_obj))
end
def event_dispatch(event_ptr_i)
import introspect
var event_ptr = introspect.toptr(event_ptr_i) # convert to comptr, because it was a pointer in the first place
if self.event self.event._p = event_ptr
else self.event = lv.lv_event(event_ptr)
end
var user_data = self.event.get_user_data() # it is supposed to be a pointer to the object
if int(user_data) != 0
var target_lvh_obj = introspect.fromptr(user_data)
if type(target_lvh_obj) == 'instance'
# print("CB Fired", self.event.code, target_lvh_obj)
target_lvh_obj.event_cb(self.event)
# print("CB Fired After")
end
end
end
#====================================================================
# Parse single object
#
# The object may be pre-existing or brand new
#====================================================================
def parse_obj(jline, page)
import global
import introspect
var obj_id = int(jline.find("id")) # id number or nil
var obj_type = jline.find("obj") # obj class or nil
obj_type = (obj_type != nil) ? str(obj_type) : nil
var lvh_page_cur = self.get_page_cur_parsing() # current page object, cannot be nil
# Step 1. Check the id for valid range
# 'obj_id' must be between 1 and 254
if (obj_id != nil) && (obj_id < 0 || obj_id > 254)
if (obj_id != 0) || (obj_type == nil)
# if `obj_id` is not `nil` and not `0`, it must have `obj_type` not set to `nil`
print(f"HSP: invalid 'id': {obj_id} for 'obj': {obj_type}")
return
end
end
# Step 2. Check if the p<>b<> object already exists
# `prev_obj` contains the pre-existing object, or `nil` if we create a new object
var obj_lvh = lvh_page_cur.get_obj(obj_id) # get reference of object or `nil` if new object
# Step 3. Create object instance if required
if (obj_type != nil) && (obj_id != nil) && (obj_lvh == nil)
# Step 3.a. extract the LVGL parent object to create the object in the appropriate lvgl screen
# Result in `parent_lvgl`
# extract haspmota class, prefix with `lvh_`. Ex: `btn` becomes `lvh_btn`
var parent_id = int(jline.find("parentid")) # id of parent object, or `nil`
var parent_obj # parent HASPmota object
var parent_lvgl # lvgl object of parent object
if parent_id != nil
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
# Step 3.b. Get the HASPmota class object for the `obj` class
# check if a class with the requested name exists
# first look for a class with name `lvh_<name>` exists
var obj_class = introspect.get(self, "lvh_" + obj_type)
var lv_instance # allows to pre-instanciate the object
# Step 3.c. if no native `lvh_<obj>` is found, try the class name from the global namespace
if obj_class == nil
# if not found, check if a LVGL class with name `lv_<name>` 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 = self.lvh_obj # use the basic lvh_obj component to encapsulate
end
end
# Step 3.d. if 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 = self.lvh_obj # use the basic lvh_obj component to encapsulate
end
end
# Step 3.e. if none found, raise an error and abort
if obj_class == nil
print(f"HSP: Cannot find object of type {obj_type}")
return
end
# Step 3.f. instanciate the object, passing the lvgl screen as parent object
obj_lvh = obj_class(parent_lvgl, page, jline, lv_instance, parent_obj)
# Step 3.g. Add object to page object
lvh_page_cur.add_obj(obj_id, obj_lvh)
end
# Step 4. if "id" is 0, get the screen object
if obj_id == 0
if (obj_type != nil)
print(f"HSP: cannot specify 'obj':'{obj_type}' for 'id':0")
return
end
obj_lvh = self.get_page_cur_parsing().get_obj(0) # get object id '0'
end
# Step 5. apply attributes
# set attributes
# try every attribute, if not supported it is silently ignored
if (obj_lvh != nil)
for k:jline.keys()
obj_lvh.(k) = jline[k]
end
end
# Step 6. apply post-config
# finally call 'post_config()' when all attributes are set, which gives an opportunity to clean or refresh
if (obj_lvh != nil)
obj_lvh.post_config()
end
# Step 7. run any Berry code embedded
# `func_compiled` contains compiled code, that will be run once the object is complete, or `nil` if no code
# `berry_run` contains the actual source code, used only for logging
var func_compiled
var berry_run = str(jline.find("berry_run"))
if berry_run != "nil"
try
func_compiled = compile(berry_run)
except .. as e,m
print(format("HSP: unable to compile berry code \"%s\" - '%s' - %s", berry_run, e, m))
end
end
if func_compiled != nil
try
# run the compiled code once
var f_ret = func_compiled()
if type(f_ret) == 'function'
f_ret(obj_lvh)
end
except .. as e,m
print(format("HSP: unable to run berry code \"%s\" - '%s' - %s", berry_run, e, m))
end
end
end
end
haspmota.HASPmota = HASPmota
#################################################################################
# General module initilization
#################################################################################
# automatically instanciate the HASPmota() monad
# note: value is cached in the module cache
# and is returned whenever you call `import haspmota` again
# This means that the object is never garbage collected
#
haspmota.init = def (m) # `init(m)` is called during first `import haspmota`
var hm = m.HASPmota
return hm()
end
#@ solidify:haspmota,weak
global.haspmota = haspmota
return haspmota