From 5f36bc2ee94516a37ab1c9e7e101fcea0fc1a553 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sat, 15 Jan 2022 23:03:24 +0100 Subject: [PATCH] LVGL calibrate touch screen --- tasmota/berry/modules/DisplayCalibrate.tapp | Bin 0 -> 12140 bytes .../berry/modules/ts_calibrate/autoexec.be | 18 + .../modules/ts_calibrate/ts_calibrate.be | 322 ++++++++++++++++++ tasmota/xdrv_54_lvgl.ino | 2 +- 4 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 tasmota/berry/modules/DisplayCalibrate.tapp create mode 100644 tasmota/berry/modules/ts_calibrate/autoexec.be create mode 100644 tasmota/berry/modules/ts_calibrate/ts_calibrate.be diff --git a/tasmota/berry/modules/DisplayCalibrate.tapp b/tasmota/berry/modules/DisplayCalibrate.tapp new file mode 100644 index 0000000000000000000000000000000000000000..7a8b80e7209e97f092b8672646ed2d9f95dd8ad3 GIT binary patch literal 12140 zcmcIq&2J<}6?eXna1LCM;DAtNMjMaU*Tku?d;I^?Va!KgsU>sYdsyD>rc+Ux%0-q|MrjYKlrZJZ|}VJ z=Fk4}`~I8X#?r3J^+jBiI#=aft7*1aglVMq-j9oA5^jDNCh>V5mU?fuyW3Ss7Df!W z$f8xERh+7YUS#=3Wofce*K?iL(8PtB#d%SxRiPtbxC(Q19jRkghQ%T)!vTIYKV3Bq zm*pzg>MBfDT4ggvDR}Z#x@*^lQ4~xUQLodawbS2KD$+C6+sA`a@nV_fr7AWB|0E)o zVL1mG7#h$c0C|d@2g_A4?_Ec=wPg`ZEh{|8Y-v|oq6%FGWxUXNZ`fB44j(Ij<)A3T zyzK1@YP%%Y#S#Zeqaeys%{1#Y+TGuM>ID3qKR-Boa?<(lZ~8ks^!*kDTtjSx{P^Ub z;d}UQ3HdBrP3O<2xrTu3LEZ*@^^5I7+iBny$f~Yc6Q665aP)+l!@T3I{%Q3zae2*$jUA4^O zv@BGrbyU#ubFD7*vh)-)WK?wM#6wsHVV)CKei%DiXX9ao-h1k*S(cXS6n_ex!ywdn z=oS^zJjn9+!h`3>uAS(P<9)`<=F`3sk%R@To~f#6gT<6Om?jy`Rmh<{b_`M{;VQ@^ z)oX|zzAU6ey7L(vOt*&uIIidMbZ&@JAvUBrB&iPoW#I2Xg4(I`+wCE#O9ySnxu1R?8*e5

E$oM*aI|dzw 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 literal 0 HcmV?d00001 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; }