blob: 4384875f994bea3572a6cdeda0ccc4efd838f4a5 [file] [log] [blame]
"""
Copyright (c) 2019, OptoFidelity OY
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 abc import ABC, abstractmethod
from collections import namedtuple
from copy import copy, deepcopy
import random
from noise.hp33120a import HP33120A
from TPPTcommon.containers import Line, Point
from TPPTcommon.ConfigurationDatabase import TestConfiguration, ConfigurationDatabase, TestConfigurationGroup
from TPPTcommon.exceptions import GridCreationError, TipSelectionError
from TPPTcommon.grid import GridVisContainer
from TPPTcommon.Node import Node
logger = logging.getLogger(__name__)
class TestStep(Node, ABC):
def __init__(self, name, context):
"""
Executed when script is loaded, child classes should define the test case controls for GUI.
"""
super().__init__(name)
self.context = context
self.display_enabled = True
self.enabled = False
self.database_configuration = None
self.results_class = None
self.measurement_timeout = 2.5
self.max_gesture_time = 60.0
self.noise_injection_needed = False
self.is_manual = False
def __repr__(self):
return self.name
def __deepcopy__(self, memo):
# Make a "deep" copy that has it's own instance of controls to be able to make configuration combinations
cls = self.__class__
step_copy = cls.__new__(cls)
memo = {id(self): step_copy}
for key, value in self.__dict__.items():
if key == 'controls':
# Currently the objects in the controls do not need to be deeply copied, but this may change if
# there are lists, dicts, or some such that also need to be independent
setattr(step_copy, key, copy(value))
else:
setattr(step_copy, key, value)
return step_copy
def save_test_controls_to_database(self, session, configuration_name, configuration_group):
if self.database_configuration is not None:
group_id = session.query(TestConfigurationGroup).filter(TestConfigurationGroup.name == configuration_group).one_or_none().id
test_configuration_id_result = session.query(TestConfiguration.id) \
.filter(TestConfiguration.name == configuration_name) \
.filter(TestConfiguration.configuration_group == group_id).one_or_none()
if test_configuration_id_result is None:
test_configuration_id = ConfigurationDatabase.create_and_insert_new_configuration(configuration_group,
configuration_name,
session)
else:
test_configuration_id = test_configuration_id_result.id
test_controls = self.controls.get_control_dictionary()
test_controls['enabled'] = self.enabled
if not self.database_configuration_exists(session, configuration_group, configuration_name):
database_object = self.database_configuration()
for key, value in test_controls.items():
if not hasattr(database_object, key):
raise AttributeError('No database field for parameter: ' + key)
else:
setattr(database_object, key, value)
database_object._test_configuration = test_configuration_id
session.add(database_object)
else:
# The configuration already exists, update the control values
session.query(self.database_configuration) \
.filter(self.database_configuration._test_configuration == test_configuration_id) \
.update(test_controls)
def database_configuration_exists(self, session, group_name, configuration_name):
group_id = session.query(TestConfigurationGroup).filter(
TestConfigurationGroup.name == group_name).one_or_none().id
existing_configuration = session.query(self.database_configuration) \
.join(self.database_configuration._test_configuration_orm) \
.filter(TestConfiguration.name == configuration_name) \
.filter(TestConfiguration.configuration_group == group_id) \
.one_or_none()
return True if existing_configuration is not None else False
def execute(self):
"""
Tell the robot to perform each gesture in turn, and record the events from the DUT.
"""
dut = self.context.get_active_dut()
self.context.html("Running %s test for dut:%s" % (self.name, dut))
test_item = self.context.create_db_test_item("%s Test" % self.name)
gestures = self._create_grid(dut)
self._initial_setup(dut)
for index, gesture in enumerate(gestures):
self.context.indicators.set_test_detail('Gesture', str(index + 1) + ' / ' + str(len(gestures)))
self._pre_gesture_setup(gesture, dut)
continuous_measurement = self.context.create_continuous_measurement(gesture)
continuous_measurement.start(timeout=self.measurement_timeout, max_time=self.max_gesture_time)
self._execute_gesture(gesture, dut)
continuous_measurement.end()
touch_list = continuous_measurement.parse_data()
gesture_id = self._create_gesture(gesture, test_item)
self._save_measurement_data(gesture_id, touch_list)
self.context.breakpoint()
self.context.close_db_test_item(test_item)
def _initial_setup(self, dut):
"""
Setup to be done one time before any gestures are executed.
"""
pass
@abstractmethod
def _pre_gesture_setup(self, gesture, dut):
"""
Setup to be done before each gesture is executed, including robot positioning.
"""
pass
@abstractmethod
def _execute_gesture(self, gesture, dut):
"""
Tell the robot to perform a single gesture.
"""
pass
def final_cleanup(self):
"""
Clean up to be done one time after all gestures have been executed, or if the test is interrupted.
"""
pass
@abstractmethod
def tip_is_valid(self):
"""
Try to find the selected tips.
:return: True if tips are found, False if not found
"""
pass
def pattern_is_valid(self):
"""
Try to create the measurement grid to see if it is possible
:return: True if the grid can be created, False if the grid cannot be created
"""
dut = self.context.get_active_dut()
try:
_ = self._create_grid(dut)
return True
except GridCreationError as e:
logger.error(str(e))
return False
@abstractmethod
def _create_grid(self, dut):
"""
Create grid of gesture shapes (lines, arcs, etc.) that define the geometry of the test case.
:param dut: DUT where the grid is evaluated on.
"""
pass
def visualize_grid(self, dut):
"""
Construct a visualization of the test case grid.
:param dut: DUT where the grid is evaluated on.
"""
test_pattern = self._create_grid(dut)
return GridVisContainer(self.__class__.__name__, (dut.width, dut.height), test_pattern, dut.name)
@abstractmethod
def _create_gesture(self, gesture, test_item):
"""
Create gesture and add to database.
:return: id of created gesture.
"""
pass
def _save_measurement_data(self, gesture_id, touch_list):
"""
Save measurements for a single gesture to database.
"""
db_list = []
self.context.clear_dut_points()
for test_result in touch_list:
results = self.results_class()
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.add_all(db_list)
def _get_finger_separation(self, clearance, tip1_size, tip2_size=None):
"""
Given a desired finger clearance (edge to edge) and finger size, return the needed finger separation (center to
center) if tips with given size were attached.
:param clearance: Desired clearance between finger tips.
:param tip1_size: Diameter of first tip (mm).
:param tip2_size: Diameter of second tip (mm). If None, assume the same as tip1.
:return: The smallest finger separation (mm).
"""
min_clearance = 0.1
if clearance < min_clearance:
raise ValueError("Finger clearance must be greater than {} mm.".format(min_clearance))
if tip2_size is None:
tip2_size = tip1_size
separation = (tip1_size + tip2_size) / 2.0 + clearance
# TODO (b/148627899): Poll the robot for minimum separation
min_robot_separation = 10.5
if separation < min_robot_separation:
raise ValueError("Finger separation must be greater than {} mm.".format(min_robot_separation))
return separation
def set_controls(tests, configurations):
tests_with_set_controls = []
for configuration in configurations:
test = [test for test in tests if test.database_configuration == type(configuration)].pop()
controlled_test = deepcopy(test)
# Filter away the attributes that start with an underscore, because these are not controls we want to set
configuration_dictionary = {key: value for key, value in configuration.__dict__.items() if
not str(key).startswith('_')}
for key, value in configuration_dictionary.items():
if key == 'enabled':
controlled_test.enabled = bool(value)
continue
if hasattr(controlled_test.controls, key):
attr_type = type(getattr(controlled_test.controls, key))
# Set control value. Cast value to the type that the value is initialized in test case init.
controlled_test.controls.__setattr__(key, attr_type(value))
else:
raise AttributeError('No control with key "{}"'.format(key))
tests_with_set_controls.append(controlled_test)
return tests_with_set_controls
class NoiseTestStep(TestStep):
"""
Abstract class for tests which use the function generator to inject noise.
WARNING: All noise tests should only use one finger, and must be careful to leave the robot at azimuth 0 throughout
test execution, because rotating the azimuth may cause damage while the function generator is attached.
When creating noise tests, the robot should delay for one second with the finger touching the surface before
performing the gesture. This is to allow the touch panel time to adapt to the noise. The first second of data
should then be ignored in analysis.
For all noise tests, the same gesture is performed repeatedly while a square wave of 10V is applied through the
tip with frequencies between 0 and 500,000 Hz in increments of 500 Hz.
"""
def __init__(self, name, context):
super().__init__(name, context)
self.noise_injection_needed = True
sizes = list(map(str, sorted(context.tips_node.single_tips_by_size.keys())))
self.controls.finger_size = sizes[0] if len(sizes) > 0 else ""
self.controls.info['finger_size'] = {'label': 'Finger size (mm)', 'items': sizes}
self.controls.freq_step = 500
self.controls.info['freq_step'] = {'label': 'Noise frequency step (Hz)', 'min': 500}
self.controls.device_path = 'COM2'
self.controls.info['device_path'] = {'label': 'Function generator device path'}
self.speed = 100.0
self.generator = None
self.test_gesture = None
self.start_point = None
def _initial_setup(self, dut):
self.test_gesture = self._single_gesture_geometry(dut)
if isinstance(self.test_gesture, Point):
self.start_point = self.test_gesture
elif isinstance(self.test_gesture, Line):
self.start_point = Point(self.test_gesture.start_x, self.test_gesture.start_y, self.test_gesture.start_z)
else:
raise ValueError("Unsupported gesture type for Noise test")
dut.move(self.start_point.x, self.start_point.y, dut.base_distance, azimuth=0)
self.context.toggle_pause()
self.context.ui.dialog_to_continue("Please attach the function generator to the robot tip, make sure DUT is "
"grounded, and make sure the function generator is on.")
self.context.breakpoint()
self.generator = HP33120A(self.controls.device_path)
def _pre_gesture_setup(self, frequency, dut):
self.context.set_robot_default_speed()
dut.move(self.start_point.x, self.start_point.y, dut.base_distance, azimuth=0)
self.context.set_robot_speed(self.speed)
self.generator.generate_function(HP33120A.Waveforms.SQUARE, frequency, 10)
def final_cleanup(self):
self.generator = None
@abstractmethod
def _single_gesture_geometry(self, dut):
"""
Create the shape that defines the gesture that is performed at each frequency.
"""
pass
def _create_grid(self, dut):
"""
For this test, all gestures performed by the robot are identical, but the frequency of noise supplied by the
function generator differs.
"""
frequencies = list(range(self.controls.freq_step, 500001, self.controls.freq_step))
# Make the frequencies appear in the same order if using the same step. This will allow us to restart a test
# run part-way through if there is a robot failure.
random.seed(0)
random.shuffle(frequencies)
return frequencies
def visualize_grid(self, dut):
test_pattern = [self._single_gesture_geometry(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(float(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()
if len(self._create_grid(dut)) == 0:
return False
try:
_ = self._single_gesture_geometry(dut)
return True
except GridCreationError as e:
logger.error(str(e))
return False
class ManualTestStep(TestStep):
"""
Abstract class for tests where the operator is required to draw on the touch surface, with no robot gestures.
"""
Gesture = namedtuple('Gesture', ['gesture', 'instruction_text_subs'])
def __init__(self, name, context):
super().__init__(name, context)
self.is_manual = True
@property
@abstractmethod
def _instruction_text(self):
"""
Instruction text to be displayed for each gesture. Should allow text substitutions as provided by
Gesture.instruction_text_subs, using the String format() function.
"""
pass
@abstractmethod
def _create_grid(self):
"""
Create list of ManualTestStep.Gesture tuples, which hold both:
* the gesture shapes (lines, arcs, etc.) that define the geometry of the test case,
* and a tuple holding text substitutions for the instruction text.
"""
pass
def _initial_setup(self, dut):
dut.move(0.0, 0.0, 30.0, azimuth=0.0)
def _pre_gesture_setup(self, gesture_tuple, dut):
self.context.toggle_pause()
self.context.ui.dialog_to_continue("Click Continue, and then: " +
self._instruction_text.format(*gesture_tuple.instruction_text_subs))
self.context.breakpoint()
self._draw_target_gesture(gesture_tuple.gesture, dut)
def _execute_gesture(self, gesture_tuple, dut):
pass
def visualize_grid(self, dut):
test_pattern = [gesture_tuple.gesture for gesture_tuple in self._create_grid(dut)]
return GridVisContainer(self.__class__.__name__, (dut.width, dut.height), test_pattern, dut.name)
def _draw_target_gesture(self, gesture, dut):
if isinstance(gesture, Line):
self._draw_target_line(gesture, dut)
else:
raise ValueError("Unsupported gesture type for manual test")
def _draw_target_line(self, line, dut):
dut_node = self.context.get_active_dut_node()
n_points = 100
x_scale = dut_node.resolution[0] / (dut.width * n_points)
y_scale = dut_node.resolution[1] / (dut.height * n_points)
self.context.clear_dut_points()
for i in range(n_points + 1):
x = x_scale * (line.start_x * (n_points - i) + line.end_x * i)
y = y_scale * (line.start_y * (n_points - i) + line.end_y * i)
self.context.ui.add_dut_point(x, y)
def tip_is_valid(self):
return True