blob: bccbb9ab3c63fe944d19e71370cd001aaa70ccd6 [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.
"""
from sqlalchemy import Column, Integer, String, Float, DateTime, ForeignKey, Boolean, Text, BLOB
from sqlalchemy.orm import relation, backref, sessionmaker
from sqlalchemy.ext.declarative import declarative_base, declared_attr
from sqlalchemy import create_engine, event
from sqlalchemy.exc import IntegrityError
import json
import time
import sqlite3
import logging
Base = declarative_base()
logger = logging.getLogger(__name__)
database_path = None
config_path = None
# The global database singleton instance
database = None
def setup_database(db_path, conf_path):
global database_path, config_path
database_path, config_path = db_path, conf_path
def get_database():
""" Returns the global database instance """
global database
if database is None:
db_path = get_last_path() if database_path is None else database_path
database = ResultDatabase(db_path)
return database
def on_connect(conn, record):
conn.execute('pragma foreign_keys=ON')
def save_last_path(path):
with open(config_path, 'w') as f:
f.write(json.dumps({"lastDB": path}))
def get_last_path():
try:
with open(config_path, 'r') as f:
data = json.load(f)
return data.get('lastDB', database_path)
except FileNotFoundError as e:
logger.error(str(e))
return database_path
def create_sqlite_indices(database_file, indices):
""" Creates indices to the SQLite database """
db = sqlite3.connect(database_file)
curindices = get_current_indices(db)
for index_values in indices:
# For some reason curindices has '_fkey' appended to names
index_name = index_values[0] + '_fkey'
if index_name not in curindices:
logging.info('Creating database index %s' % index_name)
create_index(db, index_name, index_values)
db.close()
def get_current_indices(database):
""" Returns an array of currently declared indices in the database """
c = database.cursor()
c.execute("SELECT name FROM sqlite_master WHERE type = 'index'")
return [r[0] for r in c.fetchall()]
def create_index(database, index_name, index_values):
""" Creates the specified index in the database """
database.execute('CREATE INDEX IF NOT EXISTS %s ON %s ( %s )' % (index_name, index_values[0], index_values[1]))
class ResultDatabase:
def __init__(self, filename):
self.db = None
self.session = None
self.dbpath = None
self.initialize(filename)
save_last_path(filename)
def initialize(self, filename):
self.dbpath = filename
self.db = create_engine('sqlite:///' + filename)
event.listen(self.db, 'connect', on_connect)
self.session = sessionmaker(bind=self.db, autoflush=False)
Base.metadata.create_all(self.db)
# These are database indices for various test and test result tables.
indices = [
('dut_parameters', 'dut_id'),
('session_parameters', 'testsession_id'),
('test_item', 'testsession_id'),
('test_result', 'test_id'),
]
for test in BaseTest.__subclasses__():
indices.append((test.__tablename__, 'test_id'))
for result in BaseResults.__subclasses__():
indices.append((result.__tablename__, 'gesture_id'))
create_sqlite_indices(filename, indices)
# Check if ResultDatabase already contains test types below.
try:
# TODO (b/148627899): It would be nice if these test type names were defined in respective test case Python
# files. However, do we want the ID values to be unique if test case order or number changes? Perhaps
# hash from name?
# Note: TestType integer IDs must match the ones used in analysis.
session = self.session()
session.add(TestType(0, 'Finger Tracking Test'))
session.add(TestType(1, 'Finger To Edge Test'))
session.add(TestType(2, 'Swipe Test'))
session.add(TestType(3, 'Pinch Test'))
session.add(TestType(4, 'Tap Test'))
session.add(TestType(5, 'Repeated Taps Test'))
session.add(TestType(6, 'Stationary Then Move Test'))
session.add(TestType(7, 'Drumroll Test'))
session.add(TestType(8, 'Compass Test'))
session.add(TestType(9, 'Compass With Taps Test'))
session.add(TestType(10, 'Stationary With Taps Test'))
session.add(TestType(11, 'Single Line With Noise Test'))
session.add(TestType(12, 'Stationary With Noise Test'))
session.add(TestType(13, 'Thumb Test'))
session.commit()
session.close()
except IntegrityError:
# Already created
pass
self.check_setting(
Setting('max_linearity', 2.0, 'mm', 'Max linearity error limit'))
self.check_setting(
Setting('rms_linearity', 0.5, 'mm', 'RMS linearity error limit'))
self.check_setting(
Setting('max_circularity', 2.0, 'mm', 'Max circularity error limit'))
self.check_setting(
Setting('rms_circularity', 0.5, 'mm', 'RMS circularity error limit'))
self.check_setting(
Setting('angle_error', 4.0, 'deg', 'Maximum angle error for swipe'))
self.check_setting(
Setting('gap_ratio', 1.8, '', 'Maximum ratio of gaps between adjacent touch reports'))
self.check_setting(
Setting('reversed_motion', 1.0, 'mm', 'Maximum reversed motion during swipe'))
self.check_setting(
Setting('report_rate', 60.0, 'Hz', 'Minimum report rate'))
self.check_setting(
Setting('stationary_movement', 1.0, 'mm', 'Maximum movement for stationary touches'))
self.check_setting(
Setting('minimum_radius', 0.5, 'mm', 'Minimum enclosing radius for repeated taps'))
self.check_setting(
Setting('drumroll_radius', 2.0, 'mm', 'Minimum enclosing radius for any drumroll location'))
self.check_setting(
Setting('edge_range', 0.5, 'mm', 'Maximum gap for reporting from screen edge'))
self.check_setting(
Setting('swipe_length', 1.0, 'mm', 'Maximum error for length of swipe'))
self.check_setting(
Setting('packet_count', 3, '', 'Minimum packet count'))
self.check_setting(
Setting('movement_hysteresis', 2.0, '', 'Maximum ratio (d1/d2) of distance moved for a previously '
'stationary finger, where d1 is the distance from the first to '
'second positions and d2 is the distance from the second to third '
'positions'))
# When adding settings, remember to add them to settings.py categories in the TPPT analysis repo
def change_database(self, path):
self.initialize(path)
save_last_path(path)
def add(self, data, commit=True, expire_on_commit=True):
session = self.session(expire_on_commit=expire_on_commit)
session.add(data)
if commit:
self.session_commit(session)
try:
return data.id
finally:
session.close()
def add_all(self, data, commit=True):
session = self.session()
session.add_all(data)
if commit:
self.session_commit(session)
session.close()
def get_engine(self):
return self.db
def update(self, data):
session = self.session()
session.merge(data)
self.session_commit(session)
session.close()
def commit(self):
session = self.session()
self.session_commit(session)
session.close()
def session_commit(self, session):
# If database is locked, try again until timeout occurs
max_wait_time = 15
start = time.time()
while True:
try:
session.commit()
break
except sqlite3.OperationalError as e:
if time.time() - start > max_wait_time:
raise e
def get_test_sessions(self, db_session=None):
if db_session is None:
db_session = self.session()
try:
return db_session.query(TestSession).all()
finally:
db_session.close()
def get_test_session(self, session_id):
session = self.session()
try:
return session.query(TestSession).filter_by(id=session_id).first()
finally:
session.close()
def get_test_type(self, type_id):
session = self.session()
try:
return session.query(TestType).filter_by(id=type_id).all()
finally:
session.close()
def get_test_types(self):
session = self.session()
try:
return session.query(TestType).all()
finally:
session.close()
def get_programs(self):
session = self.session()
try:
return session.query(TestSession.program).order_by(TestSession.program)
finally:
session.close()
def get_manufacturers(self):
session = self.session()
try:
return session.query(TestSession.manufacturer).order_by(TestSession.manufacturer).all()
finally:
session.close()
def check_setting(self, setting):
session = self.session()
db_setting = session.query(Setting).filter_by(id=setting.id).first()
if db_setting is None:
# Setting does not exist
session.add(setting)
self.session_commit(session)
session.close()
# Define database tables
class TestSession(Base):
# TestSession is constant to all test cases
__tablename__ = 'test_session'
id = Column(Integer, primary_key=True)
operator = Column(String)
starttime = Column(String)
endtime = Column(String)
invalid = Column(Boolean)
notes = Column(String)
tnt_version = Column(String)
tppt_version = Column(String)
station_id = Column(String)
class TestDUT(Base):
# A DUT that is tested in one or multiple test sessions and referenced to in Test Items
__tablename__ = 'test_dut'
id = Column(Integer, primary_key=True)
# Dut parameters
program = Column(String)
manufacturer = Column(String)
batch = Column(String)
serial = Column(String)
sample_id = Column(String)
svg_data = Column(BLOB)
class TestType(Base):
# Device digitizer touch test types stored here
__tablename__ = 'test_type'
id = Column(Integer, primary_key=True)
name = Column(String)
def __init__(self, _id, name):
self.id = _id
self.name = name
class TestItem(Base):
# A single test related to device digitizer touch is defined here
__tablename__ = 'test_item'
id = Column(Integer, primary_key=True)
testsession_id = Column(Integer, ForeignKey('test_session.id', ondelete='CASCADE'), nullable=False)
testsession = relation(TestSession, backref=backref('test_items', order_by=id))
dut_id = Column(Integer, ForeignKey('test_dut.id'), nullable=False)
dut = relation(TestDUT, backref=backref('test_items', order_by=id))
starttime = Column(String)
endtime = Column(String)
slot_id = Column(Integer)
finger_type = Column(String)
invalid = Column(Boolean)
kmsg_log = Column(Text)
testtype_id = Column(Integer, ForeignKey('test_type.id', ondelete='CASCADE'), nullable=False)
type = relation(TestType, backref=backref('test_items', order_by=id))
class SessionParameters(Base):
__tablename__ = 'session_parameters'
id = Column(Integer, primary_key=True)
testsession_id = Column(Integer, ForeignKey('test_session.id', ondelete='CASCADE'), nullable=False)
testsession = relation(TestSession, backref=backref('session_parameters', order_by=id))
name = Column(String)
valueFloat = Column(Float)
valueString = Column(String)
isFloat = Column(Boolean)
class DutParameters(Base):
__tablename__ = 'dut_parameters'
id = Column(Integer, primary_key=True)
dut_id = Column(Integer, ForeignKey('test_dut.id', ondelete='CASCADE'), nullable=False)
dut = relation(TestDUT, backref=backref('dut_parameters', order_by=id))
name = Column(String)
valueFloat = Column(Float)
valueString = Column(String)
isFloat = Column(Boolean)
class Setting(Base):
__tablename__ = 'settings'
id = Column(String, primary_key=True)
value = Column(Float, nullable=False)
unit = Column(String)
desc = Column(String)
def __init__(self, _id, value, unit, desc):
self.id = _id
self.value = value
self.unit = unit
self.desc = desc
class TestResult(Base):
__tablename__ = 'test_result'
id = Column(Integer, primary_key=True)
test_id = Column(Integer, ForeignKey('test_item.id', ondelete='CASCADE'), nullable=False)
test = relation(TestItem, backref=backref('test_results'))
result = Column(String)
calculated = Column(DateTime)
def camel_to_snake(camel):
snake = [camel[0].lower()]
for c in camel[1:]:
if c.isupper():
snake.append('_')
snake.append(c.lower())
return ''.join(snake)
class BaseTest(object):
""" Mixin class for test gestures.
Each test case should have a table definition using this mixin, defining a single
gesture performed by the robot. The class must be named XXXXTest, and there must
be a corresponding XXXXResults class defined to hold measurement results. This mixin
and the BaseResults mixin set up the mappings between the database tables.
"""
@declared_attr
def __tablename__(cls):
if not cls.__name__.endswith('Test'):
raise ValueError('BaseTest class names must end in "Test"')
return camel_to_snake(cls.__name__)
@declared_attr
def test_id(cls):
return Column(Integer, ForeignKey('test_item.id', ondelete='CASCADE'), nullable=False)
@declared_attr
def test(cls):
return relation(TestItem, backref=backref(cls.__tablename__, order_by=cls.id))
id = Column(Integer, primary_key=True)
class BaseResults(object):
""" Mixin class for test results.
Each test case should have a table definition using this mixin, defining a single
touch measurement. The class must be named XXXXResults, and there must be a corresponding
XXXXTest class defined. This mixin sets up the mappings between the tables, and defines the
common results fields.
"""
def _test_table_name(cls):
return cls.__tablename__[:cls.__tablename__.rfind('results')] + 'test'
def _test_class_name(cls):
return cls.__name__[:cls.__name__.rfind('Results')] + 'Test'
@declared_attr
def __tablename__(cls):
if not cls.__name__.endswith('Results'):
raise ValueError('BaseResults class names must end in "Results"')
return camel_to_snake(cls.__name__)
@declared_attr
def gesture_id(cls):
return Column(Integer, ForeignKey(cls._test_table_name(cls) + '.id', ondelete='CASCADE'), nullable=False)
@declared_attr
def gesture(cls):
return relation(cls._test_class_name(cls), backref=backref(cls.__tablename__, order_by=cls.id))
id = Column(Integer, primary_key=True)
panel_x = Column(Float)
panel_y = Column(Float)
pressure = Column(Float)
finger_id = Column(Integer)
time = Column(Float)
event = Column(Integer)
class CompassTest(Base, BaseTest):
center_x = Column(Float)
center_y = Column(Float)
finger_size = Column(Float)
separation = Column(Float)
start_azimuth = Column(Float)
end_azimuth = Column(Float)
class CompassResults(Base, BaseResults):
pass
class CompassWithTapsTest(Base, BaseTest):
center_x = Column(Float)
center_y = Column(Float)
finger_size = Column(Float)
separation = Column(Float)
start_azimuth = Column(Float)
end_azimuth = Column(Float)
tap_azimuth_step = Column(Float)
class CompassWithTapsResults(Base, BaseResults):
pass
class DrumrollTest(Base, BaseTest):
center_x = Column(Float)
center_y = Column(Float)
finger_size = Column(Float)
separation = Column(Float)
azimuth = Column(Float)
number_taps = Column(Integer)
class DrumrollResults(Base, BaseResults):
pass
class FingerToEdgeTest(Base, BaseTest):
# 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)
class FingerToEdgeResults(Base, BaseResults):
pass
class FingerTrackingTest(Base, BaseTest):
# 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 FingerTrackingResults(Base, BaseResults):
pass
class PinchTest(Base, BaseTest):
center_x = Column(Float)
center_y = Column(Float)
finger_size = Column(Float)
start_separation = Column(Float)
end_separation = Column(Float)
azimuth = Column(Float)
class PinchResults(Base, BaseResults):
pass
class RepeatedTapsTest(Base, BaseTest):
tap_x = Column(Float)
tap_y = Column(Float)
number_taps = Column(Integer)
finger_size = Column(Float)
edge_width = Column(Float)
class RepeatedTapsResults(Base, BaseResults):
pass
class SingleLineWithNoiseTest(Base, BaseTest):
start_x = Column(Float)
start_y = Column(Float)
end_x = Column(Float)
end_y = Column(Float)
finger_size = Column(Float)
frequency = Column(Float)
class SingleLineWithNoiseResults(Base, BaseResults):
pass
class StationaryThenMoveTest(Base, BaseTest):
start_x = Column(Float)
start_y = Column(Float)
end_x = Column(Float)
end_y = Column(Float)
finger_size = Column(Float)
swipe_speed = Column(Float)
class StationaryThenMoveResults(Base, BaseResults):
pass
class StationaryWithNoiseTest(Base, BaseTest):
tap_x = Column(Float)
tap_y = Column(Float)
finger_size = Column(Float)
frequency = Column(Float)
class StationaryWithNoiseResults(Base, BaseResults):
pass
class StationaryWithTapsTest(Base, BaseTest):
center_x = Column(Float)
center_y = Column(Float)
finger_size = Column(Float)
separation = Column(Float)
start_azimuth = Column(Float)
end_azimuth = Column(Float)
tap_azimuth_step = Column(Float)
class StationaryWithTapsResults(Base, BaseResults):
pass
class SwipeTest(Base, BaseTest):
# 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(Base, BaseResults):
pass
class TapTest(Base, BaseTest):
# For multi-finger taps, these are the coordinates of the primary finger (left-most with
# zero azimuth)
tap_x = Column(Float)
tap_y = Column(Float)
separation = Column(Float)
azimuth = Column(Float)
num_fingers = Column(Integer)
finger_size = Column(Float)
edge_width = Column(Float)
class TapResults(Base, BaseResults):
pass
class ThumbTest(Base, BaseTest):
# 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)
class ThumbResults(Base, BaseResults):
pass