Add Finger To Edge test

Adds the Finger To Edge test case, including configuration controls,
gesture grid generation, issuing robot commands, and saving results to
sqlite database.

BUG=b:148627899
TEST=Run test using TnT UI, selecting different configuration options.
Test grid visualization is correct, robot performs gestures correctly,
and results are saved properly in sqlite database.

Change-Id: Icd1e29d66d257bfcb7cdc82e213fda783dec631b
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/optofidelity_TPPT/+/2102950
Reviewed-by: Andrew de los Reyes <adlr@chromium.org>
Tested-by: Sean O'Brien <seobrien@chromium.org>
diff --git a/MeasurementDB.py b/MeasurementDB.py
index 87a9eee..451b830 100644
--- a/MeasurementDB.py
+++ b/MeasurementDB.py
@@ -141,6 +141,7 @@
             session.add( TestType( 16, 'Stationary Jitter Static Noise Test'))
             session.add( TestType( 17, 'One Finger Tapping Repeatability Test'))
             session.add( TestType( 18, 'Finger Tracking Test'))
+            session.add( TestType( 19, 'Finger To Edge Test'))
             session.commit()
             session.close()
         except:
diff --git a/TPPTcommon/ConfigurationDatabase.py b/TPPTcommon/ConfigurationDatabase.py
index de13fda..dd1349c 100644
--- a/TPPTcommon/ConfigurationDatabase.py
+++ b/TPPTcommon/ConfigurationDatabase.py
@@ -312,6 +312,9 @@
     _finger_tracking_configurations = \
         relationship('FingerTrackingTestParameters', back_populates='_test_configuration_orm')
 
+    _finger_to_edge_configurations = \
+        relationship('FingerToEdgeTestParameters', back_populates='_test_configuration_orm')
+
     __table_args__ = (UniqueConstraint('name', 'configuration_group', name='_name_group_uc'),)
 
 
diff --git a/TPPTcommon/grid.py b/TPPTcommon/grid.py
index a720456..1daff1c 100644
--- a/TPPTcommon/grid.py
+++ b/TPPTcommon/grid.py
@@ -1249,6 +1249,27 @@
     return retval
 
 
+def create_center_to_edge_lines(dut, border_width):
+    """
+    Create lines from the center to past each edge
+    :param dut: tnt_dut object
+    :param border_width: distance from edge to stop (mm)
+    :return: List of Container.Lines
+    """
+    retval = []
+
+    w_ = dut.width
+    h_ = dut.height
+    center_x = w_ / 2.0
+    center_y = h_ / 2.0
+    retval.append(Containers.Line(center_x, center_y, 0, border_width, center_y, 0))
+    retval.append(Containers.Line(center_x, center_y, 0, w_ - border_width, center_y, 0))
+    retval.append(Containers.Line(center_x, center_y, 0, center_x, border_width, 0))
+    retval.append(Containers.Line(center_x, center_y, 0, center_x, h_ - border_width, 0))
+
+    return retval
+
+
 def create_center_points(dut, number_of_points, border_width):
     """
     Checks which function should be used for creating random center points
diff --git a/testcases/FingerToEdge.py b/testcases/FingerToEdge.py
new file mode 100644
index 0000000..c7dc21f
--- /dev/null
+++ b/testcases/FingerToEdge.py
@@ -0,0 +1,218 @@
+"""
+Copyright (c) 2020, OptoFidelity OY
+Copyright (c) 2020, The Chromium OS Authors
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+    2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+    3. All advertising materials mentioning features or use of this software must display the following acknowledgement: This product includes software developed by the OptoFidelity OY.
+    4. Neither the name of the OptoFidelity OY nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+import logging
+
+from sqlalchemy import Column, Integer, Boolean, Float, ForeignKey, VARCHAR
+from sqlalchemy.orm import backref, relation, relationship
+
+
+import MeasurementDB as mDB
+from TPPTcommon.ConfigurationDatabase import ConfigurationDatabase
+from TPPTcommon.exceptions import GridCreationError, TipSelectionError
+from TPPTcommon.grid import GridVisContainer, create_center_to_edge_lines
+from TPPTcommon.TestStep import TestStep
+
+
+logger = logging.getLogger(__name__)
+
+# Database table name for the test case.
+DB_TEST_TABLE_NAME = 'finger_to_edge_test'
+
+# Database table name for the test results.
+DB_RESULTS_TABLE_NAME = 'finger_to_edge_results'
+
+# Database table indices associated with test case.
+DB_TABLE_INDICES = [(DB_TEST_TABLE_NAME, 'test_id'), (DB_RESULTS_TABLE_NAME, 'gesture_id')]
+
+
+class FingerToEdgeTest(mDB.Base):
+    __tablename__ = DB_TEST_TABLE_NAME
+
+    id = Column(Integer, primary_key=True)
+    test_id = Column(Integer, ForeignKey('test_item.id', ondelete='CASCADE'), nullable=False)
+    test = relation(mDB.TestItem, backref=backref(DB_TEST_TABLE_NAME, order_by=id))
+
+    # For multi-finger lines, these are the coordinates of the primary finger (left-most with
+    # zero azimuth)
+    start_x = Column(Float)
+    start_y = Column(Float)
+    end_x = Column(Float)
+    end_y = Column(Float)
+
+    finger_size = Column(Float)
+
+
+class FingerToEdgeResults(mDB.Base):
+    __tablename__ = DB_RESULTS_TABLE_NAME
+
+    id = Column(Integer, primary_key=True)
+    gesture_id = Column(Integer, ForeignKey(DB_TEST_TABLE_NAME + '.id', ondelete='CASCADE'), nullable=False)
+    line = relation(FingerToEdgeTest, backref=backref(DB_RESULTS_TABLE_NAME, order_by=id))
+
+    panel_x = Column(Float)
+    panel_y = Column(Float)
+    pressure = Column(Float)
+    finger_id = Column(Integer)
+    time = Column(Float)
+    event = Column(Integer)
+
+
+class FingerToEdge(TestStep):
+    """
+    In this test case we draw a line from the center of the surface to each edge, going completely over the edge.
+    """
+
+    def __init__(self, context):
+        super().__init__('Finger To Edge')
+
+        self.context = context
+
+        # If database_configuration is defined, the controls parameters will be overridden from database if possible
+        self.database_configuration = FingerToEdgeTestParameters
+
+        sizes = sorted(list(context.tips_node.single_tips_by_size.keys()), key=float)
+
+        self.controls.finger_size = ""
+        self.controls.info['finger_size'] = {'label': 'Finger size (mm)', 'items': sizes}
+
+        self.speed = 30.0
+
+    def execute(self):
+        dut = self.context.get_active_dut()
+
+        base_distance = dut.base_distance
+
+        self.context.html("Running Finger To Edge test for dut:%s" % dut)
+
+        test_item = self.context.create_db_test_item("Finger To Edge Test")
+
+        measurement_lines = self._create_grid(dut)
+
+        for index, line in enumerate(measurement_lines):
+            self.context.indicators.set_test_detail('Line', str(index + 1) + ' / ' + str(len(measurement_lines)))
+
+            self.context.set_robot_default_speed()
+            dut.jump(line.start_x, line.start_y, base_distance, base_distance)
+
+            self.context.set_robot_speed(self.speed)
+
+            continuous_measurement = self.context.create_continuous_measurement(line)
+            continuous_measurement.start()
+
+            dut.drag(line.start_x, line.start_y, line.end_x, line.end_y, clearance=-2)
+
+            continuous_measurement.end()
+
+            touch_list = continuous_measurement.parse_data()
+
+            gesture_id = self._create_gesture(line, test_item)
+
+            self._save_measurement_data(gesture_id, touch_list)
+
+            self.context.breakpoint()
+
+        self.context.close_db_test_item(test_item)
+
+    def _create_grid(self, dut):
+        """
+        Create grid of lines that define the geometry of the test case.
+        :param dut: DUT where the grid is evaluated on.
+        """
+        border_extension = 10.0
+
+        grid = create_center_to_edge_lines(dut, -border_extension)
+
+        return grid
+
+    def visualize_grid(self, dut):
+        test_pattern = self._create_grid(dut)
+        return GridVisContainer(self.__class__.__name__, (dut.width, dut.height), test_pattern, dut.name)
+
+    def tip_is_valid(self):
+        try:
+            self.context.tips_node.select_tips_by_size(self.controls.finger_size, 1, check_only=True)
+            return True
+        except TipSelectionError as e:
+            logger.error(str(e))
+            return False
+
+    def pattern_is_valid(self):
+        dut = self.context.get_active_dut()
+        try:
+            _ = self._create_grid(dut)
+            return True
+        except GridCreationError as e:
+            logger.error(str(e))
+            return False
+
+    def _create_gesture(self, line, test_item):
+        """
+        Create gesture and add to database.
+        :return: id of created gesture.
+        """
+        test = FingerToEdgeTest()
+
+        test.start_x = line.start_x
+        test.start_y = line.start_y
+        test.end_x = line.end_x
+        test.end_y = line.end_y
+        test.test_id = test_item.id
+        test.finger_size = float(self.controls.finger_size)
+
+        self.context.db.add(test)
+
+        return test.id
+
+    def _save_measurement_data(self, gesture_id, touch_list):
+        """
+        Save finger to edge measurement to database.
+        """
+        db_list = []
+
+        for test_result in touch_list:
+            results = FingerToEdgeResults()
+
+            results.panel_x = float(test_result.panel_x)
+            results.panel_y = float(test_result.panel_y)
+            results.pressure = float(test_result.pressure)
+            results.finger_id = int(test_result.finger_id)
+            results.time = test_result.time
+            results.event = test_result.event
+            results.gesture_id = gesture_id
+
+            db_list.append(results)
+
+            self.context.add_dut_point(float(test_result.panel_x), float(test_result.panel_y))
+
+        self.context.db.addAll(db_list)
+
+
+class FingerToEdgeTestParameters(ConfigurationDatabase.TestConfigBase):
+    __tablename__ = 'finger_to_edge_parameters'
+    _id = Column('id', Integer, primary_key=True, autoincrement=True)
+    _test_configuration = Column('test_configuration', Integer,
+                                 ForeignKey('test_configuration.id', ondelete='CASCADE'))
+    _test_configuration_orm = relationship('TestConfiguration', back_populates='_finger_to_edge_configurations')
+
+    enabled = Column(Boolean)
+    finger_size = Column(VARCHAR(40))