Tasmota/tasmota/berry/modules/ts_calibrate/ts_calibrate.be

323 lines
11 KiB
Python

# 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