Add Swipe Test

Adds the Swipe 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: I49f7561dcb4e96fd145228a7c07bdd3e6c321f62
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/optofidelity_TPPT/+/2102951
Tested-by: Sean O'Brien <seobrien@chromium.org>
Reviewed-by: Harry Cutts <hcutts@chromium.org>
diff --git a/MeasurementDB.py b/MeasurementDB.py
index 451b830..2d33b4a 100644
--- a/MeasurementDB.py
+++ b/MeasurementDB.py
@@ -142,6 +142,7 @@
             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.add( TestType( 20, 'Swipe Test'))
             session.commit()
             session.close()
         except:
diff --git a/TPPTcommon/ConfigurationDatabase.py b/TPPTcommon/ConfigurationDatabase.py
index dd1349c..2f1b4e2 100644
--- a/TPPTcommon/ConfigurationDatabase.py
+++ b/TPPTcommon/ConfigurationDatabase.py
@@ -315,6 +315,9 @@
     _finger_to_edge_configurations = \
         relationship('FingerToEdgeTestParameters', back_populates='_test_configuration_orm')
 
+    _swipe_configurations = \
+        relationship('SwipeTestParameters', back_populates='_test_configuration_orm')
+
     __table_args__ = (UniqueConstraint('name', 'configuration_group', name='_name_group_uc'),)
 
 
diff --git a/testcases/Swipe.py b/testcases/Swipe.py
new file mode 100644
index 0000000..5fae394
--- /dev/null
+++ b/testcases/Swipe.py
@@ -0,0 +1,242 @@
+"""
+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 (create_multi_swipe_horizontal, create_multi_swipe_vertical,
+                             create_multi_swipe_diagonal, GridVisContainer)
+from TPPTcommon.TestStep import TestStep
+
+logger = logging.getLogger(__name__)
+
+# Database table name for the test case.
+DB_TEST_TABLE_NAME = 'swipe_test'
+
+# Database table name for the test results.
+DB_RESULTS_TABLE_NAME = 'swipe_results'
+
+# Database table indices associated with test case.
+DB_TABLE_INDICES = [(DB_TEST_TABLE_NAME, 'test_id'), (DB_RESULTS_TABLE_NAME, 'gesture_id')]
+
+
+class SwipeTest(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)
+
+    num_fingers = Column(Integer)
+    finger_size = Column(Float)
+    swipe_speed = Column(Float)
+    separation = Column(Float)
+    azimuth = Column(Float)
+
+
+class SwipeResults(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(SwipeTest, 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 Swipe(TestStep):
+    """
+    In this test case we perform fast swipes across the DUT using one or two fingers, which approach the touch
+    surface while already in motion.
+    """
+
+    def __init__(self, context):
+        super().__init__('Swipe')
+
+        self.context = context
+
+        # If database_configuration is defined, the controls parameters will be overridden from database if possible
+        self.database_configuration = SwipeTestParameters
+
+        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.controls.num_fingers = 1
+        self.controls.info['num_fingers'] = {'label': 'Number of Fingers', 'min': 1, 'max': 2}
+
+        self.speed = 300.0
+
+    def execute(self):
+        dut = self.context.get_active_dut()
+
+        base_distance = dut.base_distance
+        separation = self.context.get_min_separation(float(self.controls.finger_size))
+
+        self.context.html("Running Swipe test for dut:%s" % dut)
+
+        test_item = self.context.create_db_test_item("Swipe 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()
+
+            self.context.robot.set_finger_separation(separation)
+            dut.swipe(line.start_x, line.start_y, line.end_x, line.end_y, clearance=-2, radius=20,
+                      azimuth1=line.angle, azimuth2=line.angle)
+
+            continuous_measurement.end()
+
+            touch_list = continuous_measurement.parse_data()
+
+            gesture_id = self._create_gesture(line, test_item, separation)
+
+            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.
+        """
+        separation = self.context.get_min_separation(float(self.controls.finger_size))
+        border_width = 10.0
+
+        grid = create_multi_swipe_horizontal(dut, self.controls.num_fingers, separation, border_width)
+        grid.extend(create_multi_swipe_vertical(dut, self.controls.num_fingers, separation, border_width))
+        grid.extend(create_multi_swipe_diagonal(dut, self.controls.num_fingers, separation, border_width))
+
+        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):
+        # TODO: allow multiple fingers when support is added in robot client
+        if self.controls.num_fingers > 1:
+            logger.error("Swipes with more than one finger are not yet supported.")
+            return False
+
+        try:
+            self.context.tips_node.select_tips_by_size(self.controls.finger_size, self.controls.num_fingers,
+                                                       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, separation):
+        """
+        Create gesture and add to database.
+        :return: id of created gesture.
+        """
+        test = SwipeTest()
+
+        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.separation = separation
+        test.azimuth = line.angle
+        test.test_id = test_item.id
+        test.num_fingers = line.fingers
+        test.finger_size = float(self.controls.finger_size)
+        test.swipe_speed = self.speed
+
+        self.context.db.add(test)
+
+        return test.id
+
+    def _save_measurement_data(self, gesture_id, touch_list):
+        """
+        Save swipe measurement to database.
+        """
+        db_list = []
+
+        for test_result in touch_list:
+            results = SwipeResults()
+
+            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 SwipeTestParameters(ConfigurationDatabase.TestConfigBase):
+    __tablename__ = 'swipe_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='_swipe_configurations')
+
+    enabled = Column(Boolean)
+    finger_size = Column(VARCHAR(40))
+    num_fingers = Column(Integer)