diff --git a/tasmota/berry/modules/DisplayCalibrate.tapp b/tasmota/berry/modules/DisplayCalibrate.tapp new file mode 100644 index 000000000..7a8b80e72 Binary files /dev/null and b/tasmota/berry/modules/DisplayCalibrate.tapp differ diff --git a/tasmota/berry/modules/ts_calibrate/autoexec.be b/tasmota/berry/modules/ts_calibrate/autoexec.be new file mode 100644 index 000000000..2663f2edf --- /dev/null +++ b/tasmota/berry/modules/ts_calibrate/autoexec.be @@ -0,0 +1,18 @@ +# register the command 'DisplayCalibrate' +# +# load the module in memory only when the command is first used +# +var wd = tasmota.wd # capture value of the first run +tasmota.add_cmd("DisplayCalibrate", + def () + import sys + var path = sys.path() + + path.push(wd) + import ts_calibrate + path.pop() + + tasmota.set_timer(0, /-> ts_calibrate.start()) + tasmota.resp_cmnd_done() + end +) diff --git a/tasmota/berry/modules/ts_calibrate/ts_calibrate.be b/tasmota/berry/modules/ts_calibrate/ts_calibrate.be new file mode 100644 index 000000000..8435c2199 --- /dev/null +++ b/tasmota/berry/modules/ts_calibrate/ts_calibrate.be @@ -0,0 +1,322 @@ +# TouchScreen calibration +var ts_calibrate = module("ts_calibrate") + +ts_calibrate.init = def (m) + class TS_Calibrate + var l1, l2 # LVGL lines used to draw a cross + var p1, p2, p3, p4 # points needs to be kept in memory across calls + var pt_arr1, pt_arr2 + var f20 # font Montserrat 20 + var scr_orig # original screen + var scr_ts # screen used for calibration + var hres, vres + var instr_label # label for instructions + # + var raw_pts # 4x raw points for 4 corners + var raw_pts_weight # weight for current corner, used for an aveage measure + # + var state_closure # closure to call at next iteration + var state_corner # which corner are we now tracking + # + var m_line # display.ini parsed, array of 2 string (before and after :M line) + + static msg_prefix = "Touch Screen calibration\n" + static msg_touch = "Press the center of\n" + static msg_corners = [ "upper left cross", "upper right cross", "lower left cross", "lower right cross" ] + static msg_ok = "Calibration successful\nRestarting in 5 seconds" + static msg_nok = "Calibration failed\nPlease try again" + + static round = def (v) return int(v + 0.5) end + + def init() + end + + def start() + self.state_corner= -1 # no corner being tracked yet + self.m_line = nil + + # check display.ini to see if touchscreen is present with ':M' line in display.ini + self.m_line = self.load_m_line() + if !self.m_line + tasmota.log("TS : Abort, no touchscreen ':M' line present in 'display.ini'", 2) + return + end + + lv.start() + + # check if `DisplayRotate` is set to zero + var display_rotate_ret = tasmota.cmd("DisplayRotate") + if display_rotate_ret != nil && display_rotate_ret["DisplayRotate"] != 0 + tasmota.log("TS : Calibration requires 'DisplayRotate 0'") + return + end + + self.raw_pts = [ lv.point(), lv.point(), lv.point(), lv.point() ] + + self.scr_orig = lv.scr_act() # save the current screen to restore it later + self.f20 = lv.montserrat_font(20) # load embedded Montserrat 20 + self.hres = lv.get_hor_res() + self.vres = lv.get_ver_res() + + self.scr_ts = lv.obj(0) # create a new temporary screen for the calibration + lv.scr_load(self.scr_ts) + + self.instr_label = lv.label(self.scr_ts) + self.instr_label.center() + if self.f20 != nil self.instr_label.set_style_text_font(self.f20, lv.PART_MAIN | lv.STATE_DEFAULT) end + self.instr_label.set_text(self.msg_prefix) + + self.l1 = lv.line(self.scr_ts) + self.l2 = lv.line(self.scr_ts) + self.l1.set_style_line_width(2, lv.PART_MAIN | lv.STATE_DEFAULT) + self.l2.set_style_line_width(2, lv.PART_MAIN | lv.STATE_DEFAULT) + + self.p1 = lv.point() + self.p2 = lv.point() + self.p3 = lv.point() + self.p4 = lv.point() + + # register ourselves as driver + tasmota.add_driver(self) + + # start calibrate in 2 seconds + tasmota.set_timer(2000, /-> self.do_next_corner()) + end + + def do_cross_n(n) + self.state_corner = n + self.raw_pts_weight = 0 # reset weight (for average of measures) + + if n == 0 self.draw_cross(20,20,30) + elif n == 1 self.draw_cross(self.hres - 20, 20, 30) + elif n == 2 self.draw_cross(20, self.vres - 20, 30) + else self.draw_cross(self.hres - 20, self.vres - 20, 30) + end + # set message + self.instr_label.set_text(self.msg_prefix + self.msg_touch + self.msg_corners[n]) + end + + # remove and restore previous state + def del() + lv.scr_load(self.scr_orig) # restore previous screen + self.scr_ts.del() # delete all objects + tasmota.remove_driver(self) + end + + # draw cross + def draw_cross(x, y, size) + var sz2 = size / 2 + self.p1.x = x - sz2 + self.p1.y = y + self.p2.x = x + sz2 + self.p2.y = y + + self.pt_arr1 = lv.lv_point_arr([self.p1, self.p2]) + self.l1.set_points(self.pt_arr1, 2) + + self.p3.x = x + self.p3.y = y - sz2 + self.p4.x = x + self.p4.y = y + sz2 + + self.pt_arr2 = lv.lv_point_arr([self.p3, self.p4]) + self.l2.set_points(self.pt_arr2, 2) + end + + def every_50ms() + # + if self.state_closure self.state_closure() end + end + + def track_touch_screen() + var tracking = lv.get_ts_calibration() + if tracking.state + # screen is pressed, compute an average of all previous measures + self.raw_pts[self.state_corner].x = (self.raw_pts[self.state_corner].x * self.raw_pts_weight + tracking.raw_x) / (self.raw_pts_weight + 1) + self.raw_pts[self.state_corner].y = (self.raw_pts[self.state_corner].y * self.raw_pts_weight + tracking.raw_y) / (self.raw_pts_weight + 1) + self.raw_pts_weight += 1 # we now have 1 more measure + else + # screen is not pressed anymore + if (self.raw_pts_weight >= 3) + self.state_closure = nil # stop tracking + tasmota.set_timer(0, /-> self.do_next_corner()) # defer to next corner + + end # we need at least 3 succesful measures do consider complete + end + end + + def do_next_corner() + # start the measure of the next corner + self.state_corner += 1 + if self.state_corner <= 3 + self.do_cross_n(self.state_corner) + self.state_closure = /-> self.track_touch_screen() + else + # finished + self.l1.del() + self.l2.del() + self.finish() + end + end + + # All values are computed and correct, log results and store to 'display.ini' + def finish() + # calibration is finished, do the housekeeping + import string + var p0x = real(self.raw_pts[0].x) + var p0y = real(self.raw_pts[0].y) + var p1x = real(self.raw_pts[1].x) + var p1y = real(self.raw_pts[1].y) + var p2x = real(self.raw_pts[2].x) + var p2y = real(self.raw_pts[2].y) + var p3x = real(self.raw_pts[3].x) + var p3y = real(self.raw_pts[3].y) + tasmota.log(string.format("TS : Calibration (%i,%i) - (%i,%i) - (%i,%i) - (%i,%i)", + int(p0x), int(p0y), int(p1x), int(p1y), int(p2x), int(p2y), int(p3x), int(p3y)) + , 2) + var m_line = self.calc_geometry(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, self.hres, self.vres, 20) + var ok = false + if m_line + ok = self.update_display_ini(m_line) + end + if ok + self.instr_label.set_text(self.msg_prefix + self.msg_ok) + else + self.instr_label.set_text(self.msg_prefix + self.msg_nok) + end + tasmota.set_timer(3000, /->self.cleanup(ok)) + end + + # cleanup display, remove any widgets created + def cleanup(restart) + self.del() + + if (restart) + tasmota.cmd("Restart 1") + end + end + + # Find 'display.ini' file either in root folder or in autoconf file + # and check that it contains a line ':M*' + # Returns an array of 2 strings (before and after :M line) + # or 'nil' if not found + def load_m_line() + try + import re + import path + + # try display.ini at root + var disp_ini + if path.exists("display.ini") + var disp_f = open("display.ini") + disp_ini = disp_f.read() + disp_f.close() + elif autoconf.get_current_module_path() && path.exists(autoconf.get_current_module_path() + "#display.ini") + var disp_f = open(autoconf.get_current_module_path() + "#display.ini") + disp_ini = disp_f.read() + disp_f.close() + else + return nil + end + + # look for ":M" line + var sp = re.split(":M.*?\n", disp_ini) + if size(sp) == 2 + return sp # found + end + except .. as e, m + tasmota.log("TS : Couldn't open 'display.ini': "+str(e)+" '"+str(m)+"'") + end + return nil + end + + # try to update 'display.ini' if present in file-system + def update_display_ini(m_line) + try + + # found + var disp_ini = self.m_line[0] + str(m_line) + "\n" + self.m_line[1] + + # write back file + var disp_f = open("display.ini", "w") + disp_f.write(disp_ini) + disp_f.close() + tasmota.log("TS : Successfully updated 'display.ini'", 2) + return true + except .. as e, m + tasmota.log("TS : Error updating 'display.ini': "+str(e)+" '"+str(m)+"'") + end + return false + end + + def calc_geometry(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y, hres, vres, padding) + import math + import string + + tasmota.log(string.format("TS : Geometry (%i,%i) (%i,%i) (%i,%i) (%i,%i) - %ix%i pad %i", + int(p0x), int(p0y), + int(p1x), int(p1y), + int(p2x), int(p2y), + int(p3x), int(p3y), + int(hres), int(vres), int(padding) + ), 3) + + var vec_01_x = p1x - p0x + var vec_01_y = p1y - p0y + var norm_01 = math.sqrt(vec_01_x * vec_01_x + vec_01_y * vec_01_y) + + var vec_02_x = p2x - p0x + var vec_02_y = p2y - p0y + var norm_02 = math.sqrt(vec_01_x * vec_01_x + vec_01_y * vec_01_y) + + var scalar_01_02 = vec_01_x * vec_02_x + vec_01_y * vec_02_y + var cos_th_01_02 = math.abs(scalar_01_02) / norm_01 / norm_02 + + tasmota.log("cos_th_01_02=" + str(cos_th_01_02), 4) + + if (cos_th_01_02 > 0.05) + tasmota.log("TS : Wrong geometry - bad angle. Try again.", 2) + return nil + end + + # Now check the center is valid + var center_03_x = (p0x + p3x) / 2 + var center_03_y = (p0y + p3y) / 2 + var center_12_x = (p1x + p2x) / 2 + var center_12_y = (p1y + p2y) / 2 + var norm_delta_centers = ((center_12_x - center_03_x) * (center_12_x - center_03_x) + (center_12_y - center_03_y) * (center_12_y - center_03_y)) / norm_01 / norm_02 + tasmota.log("norm_delta_centers=" + str(norm_delta_centers), 4) + + if (norm_delta_centers > 0.02) + tasmota.log("TS : Wrong geometry - bad center. Try again", 2) + return nil + end + + var xmin = (p0x + p2x) / 2 + var xmax = (p1x + p3x) / 2 + var ymin = (p0y + p1y) / 2 + var ymax = (p2y + p3y) / 2 + tasmota.log("raw xmin=" + str(xmin) + " xmax=" + str(xmax) + " ymin=" + str(ymin) + " ymax=" + str(ymax), 4) + + var range_x = xmax - xmin + var range_y = ymax - ymin + + tasmota.log("range_x=" + str(range_x) + " range_y=" + str(range_y), 4) + var extend_x = (range_x / (hres - 2*padding) * hres - range_x) / 2 + var extend_y = (range_y / (vres - 2*padding) * vres - range_y) / 2 + + xmin -= extend_x + xmax += extend_x + ymin -= extend_y + ymax += extend_y + tasmota.log("final xmin=" + str(xmin) + " xmax=" + str(xmax) + " ymin=" + str(ymin) + " ymax=" + str(ymax), 4) + + var M_string = string.format(":M,%i,%i,%i,%i", int(xmin), int(xmax), int(ymin), int(ymax)) + tasmota.log(string.format("TS : Add this to display.ini '%s'", M_string)) + return M_string + end + end + return TS_Calibrate() +end + +return ts_calibrate diff --git a/tasmota/xdrv_54_lvgl.ino b/tasmota/xdrv_54_lvgl.ino index d911bb48d..6878ea1a8 100644 --- a/tasmota/xdrv_54_lvgl.ino +++ b/tasmota/xdrv_54_lvgl.ino @@ -348,7 +348,7 @@ void start_lvgl(const char * uconfig); void start_lvgl(const char * uconfig) { if (glue != nullptr) { - AddLog(LOG_LEVEL_INFO, D_LOG_LVGL " LVGL was already initialized"); + AddLog(LOG_LEVEL_INFO, D_LOG_LVGL "LVGL was already initialized"); return; }