factory: Migrate camera/ALS test to Chrome UI

The CL migrates the camera performance and ALS calibration test
to the HTML5 based UI. Other improvements include:

- Draw the pattern registration result with the captured image
  so that the operator could be more informed about the test
  status.
- Dump the ALS calibration data to vpd in the FATP test.
- Add ALS value sanity checks.

BUG=chrome-os-partner:10321
TEST=Tested on real devices -> works.

Change-Id: I7eb25f29d9ab208c2bac6ef9823f2e21dc1220f6
Reviewed-on: https://gerrit.chromium.org/gerrit/30153
Reviewed-by: Tai-Hsu Lin <sheckylin@chromium.org>
Tested-by: Tai-Hsu Lin <sheckylin@chromium.org>
diff --git a/client/cros/camera/renderer.py b/client/cros/camera/renderer.py
new file mode 100755
index 0000000..c9cd605
--- /dev/null
+++ b/client/cros/camera/renderer.py
@@ -0,0 +1,100 @@
+# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Import guard for OpenCV.
+try:
+    import cv
+    import cv2
+except ImportError:
+    pass
+
+import math
+import numpy as np
+
+# Color constants.
+_COLORS = {
+    'important': (0, 0, 255),
+    'corner': (0, 255, 255),
+    'success': (0, 255, 0),
+    'deviation': (255, 0, 255),
+    }
+
+
+def _DrawLineByAngle(img, origin, length, angle, color,
+                     thickness):
+    '''Draw a line by specifying the origin, length and angle.'''
+    dx1 = length * math.cos(angle)
+    dy1 = length * math.sin(angle)
+    cv2.line(img, origin, (origin[0] + dx1, origin[1] + dy1), color,
+             thickness)
+
+
+def _DrawArrowTip(img, tip, direction, tip_length, tip_angle, color,
+                  thickness):
+    '''Draw an arrow tip.'''
+    theta1 = math.radians(direction + 180 + tip_angle)
+    _DrawLineByAngle(img, tip, tip_length, theta1, color, thickness)
+    theta2 = math.radians(direction + 180 - tip_angle)
+    _DrawLineByAngle(img, tip, tip_length, theta2, color, thickness)
+
+
+def _DrawArcWithArrow(img, center, radius, start_angle, delta_angle,
+                        tip_length, tip_angle, color, thickness):
+    '''Draw an arc with an arrow tip on one end.'''
+    # Draw arc.
+    cv2.ellipse(img, center, (radius, radius), start_angle,
+                (0 if delta_angle > 0 else -delta_angle),
+                (-delta_angle if delta_angle > 0 else 0), color, thickness)
+
+    # Draw arrow tip.
+    # The minus sign is because the y axis is reversed for the actual image.
+    tip_point_angle = -(start_angle + delta_angle)
+    tip = (center[0] + radius * math.cos(math.radians(tip_point_angle)),
+           center[1] + radius * math.sin(math.radians(tip_point_angle)))
+    _DrawArrowTip(img, tip,
+                  tip_point_angle - (90 if delta_angle > 0 else -90),
+                  tip_length, tip_angle, color, thickness)
+
+
+def DrawVC(img, success, result):
+    '''Draw the result of the visual correctness test on the test image.'''
+    if hasattr(result, 'sample_corners'):
+        # Draw all corners.
+        for point in result.sample_corners:
+            cv2.circle(img, (point[0], point[1]), 2, _COLORS['corner'],
+                       thickness=-1)
+
+        if hasattr(result, 'shift'):
+            # Draw the four corners of the corner grid.
+            for point in result.four_corners:
+                cv2.circle(img, (point[0], point[1]), 4,
+                           _COLORS[('success' if success else 'important')],
+                           thickness = -1)
+
+            # Draw the center and the shift vector.
+            center = ((img.shape[1] - 1) / 2.0, (img.shape[0] - 1) / 2.0)
+            tip = np.array(center) + result.v_shift
+            cv2.line(img, center, (tip[0], tip[1]),
+                     _COLORS['deviation'], thickness=2)
+            diag_len = math.sqrt(img.shape[0] ** 2 + img.shape[1] ** 2)
+            angle = math.atan2(result.v_shift[1], result.v_shift[0])
+            _DrawArrowTip(img, (tip[0], tip[1]), math.degrees(angle),
+                          result.shift * diag_len * 0.3, 60,
+                          _COLORS['deviation'], thickness=2)
+            cv2.circle(img, center, 4,
+                       _COLORS[('success' if success else 'important')],
+                       thickness=-1)
+
+            # Draw the rotation indicator.
+            radius = max(img.shape) / 4
+            # Boost the amount so it is more easily visible.
+            angle = max(-90, min(90, result.tilt * 10))
+            tip_length = abs(math.radians(angle)) * radius * 0.3
+            tip_angle = 60
+            _DrawArcWithArrow(img, center, radius, 0, angle,
+                              tip_length, tip_angle,
+                              _COLORS['deviation'], thickness=2)
+            _DrawArcWithArrow(img, center, radius, 180, angle,
+                              tip_length, tip_angle,
+                              _COLORS['deviation'], thickness=2)
diff --git a/client/site_tests/factory_CameraPerformanceAls/factory_CameraPerformanceAls.py b/client/site_tests/factory_CameraPerformanceAls/factory_CameraPerformanceAls.py
index 1e52b3a..137de39 100644
--- a/client/site_tests/factory_CameraPerformanceAls/factory_CameraPerformanceAls.py
+++ b/client/site_tests/factory_CameraPerformanceAls/factory_CameraPerformanceAls.py
@@ -3,8 +3,6 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-# TODO(sheckylin): Refactor the code with the new HTML5 framework.
-
 # Import guard for OpenCV.
 try:
     import cv
@@ -12,65 +10,32 @@
 except ImportError:
     pass
 
-import gtk
-import logging
+import base64
 import numpy as np
 import os
 import pprint
+import pyudev
 import re
+import select
 import serial
 import StringIO
+import subprocess
+import threading
 import time
 
 import autotest_lib.client.cros.camera.perf_tester as camperf
+import autotest_lib.client.cros.camera.renderer as renderer
 
 from autotest_lib.client.bin import test
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.cros import factory_setup_modules
 from cros.factory.test import factory
-from cros.factory.test import ui as ful
 from cros.factory.test import leds
-from cros.factory.test.media_util import MediaMonitor
 from cros.factory.test.media_util import MountedMedia
 from autotest_lib.client.cros.rf.config import PluggableConfig
 from autotest_lib.client.cros import tty
+from cros.factory.test.test_ui import UI
 
-_MESSAGE_USB = (
-    'Please insert the usb stick to load parameters.\n'
-    '請插入usb以讀取測試參數\n')
-_MESSAGE_PREPARE_MACHINE = (
-    'Please put the machine in the fixture and connect the keyboard.\n'
-    'Then press ENTER.\n'
-    '請將待測機器放入盒中並連接鍵盤\n'
-    '備妥後按ENTER\n')
-_MESSAGE_PREPARE_PANEL = (
-    'Please connect the next AB panel.\n'
-    'Then press ENTER to scan the barcode.\n'
-    '請連接下一塊AB Panel\n'
-    '備妥後按ENTER掃描序號\n')
-_MESSAGE_PREPARE_CAMERA = (
-    'Make sure the camera is connected\n'
-    'Then press ENTER to proceed, TAB to skip.\n'
-    '確定 攝像頭 連接完成\n'
-    '備妥後按ENTER繼續, 或按TAB跳過\n')
-_MESSAGE_PREPARE_ALS = (
-    'Make sure the light sensor is connected\n'
-    'Then press ENTER to proceed, TAB to skip.\n'
-    '確定 光感測器 連接完成\n'
-    '備妥後按ENTER繼續, 或按TAB跳過\n')
-_MESSAGE_RESULT_TAB_ABONLY = (
-    'Results are listed below.\n'
-    'Please disconnect the panel and press ENTER to write log.\n'
-    '測試結果顯示如下\n'
-    '請將AB Panel移除, 並按ENTER寫入測試結果\n')
-_MESSAGE_RESULT_TAB_FULL = (
-    'Results are listed below.\n'
-    'Please disconnect the machine and press ENTER to write log.\n'
-    '測試結果顯示如下\n'
-    '請將測試機器移除, 並按ENTER寫入測試結果\n')
-
-_TEST_SN_NUMBER = 'TEST-SN-NUMBER'
-_LABEL_SIZE = (300, 30)
 
 # Test type constants:
 _TEST_TYPE_AB = 'AB'
@@ -80,19 +45,6 @@
 _CONTENT_IMG = 'image'
 _CONTENT_TXT = 'text'
 
-def make_prepare_widget(message, on_key_enter, on_key_tab=None):
-    """Returns a widget that display the message and bind proper functions."""
-    widget = gtk.VBox()
-    widget.add(ful.make_label(message))
-    def key_release_callback(widget, event):
-        if event.keyval == gtk.keysyms.Tab:
-            if on_key_tab is not None:
-                return on_key_tab()
-        elif event.keyval == gtk.keysyms.Return:
-            return on_key_enter()
-    widget.key_callback = key_release_callback
-    return widget
-
 
 class ALS():
     '''Class to interface the ambient light sensor over iio.'''
@@ -114,6 +66,12 @@
         self.val_path = val_path
         self.scale_path = scale_path
 
+    def _read_core(self):
+        fd = open(self.val_path)
+        val = int(fd.readline().rstrip())
+        fd.close()
+        return val
+
     def _read(self, delay=None, samples=1):
         '''Read the light sensor value.
 
@@ -131,11 +89,13 @@
             delay = self._DEFAULT_MIN_DELAY
 
         buf = []
+        # The first value might be contaminated by previous settings.
+        # We need to skip it for better accuracy.
+        self._read_core()
         for dummy in range(samples):
-            fd = open(self.val_path)
-            buf.append(int(fd.readline().rstrip()))
-            fd.close()
             time.sleep(delay)
+            val = self._read_core()
+            buf.append(val)
 
         return buf
 
@@ -144,7 +104,16 @@
             return None
 
         buf = self._read(delay, samples)
-        return sum(buf) / len(buf)
+        return int(round(float(sum(buf)) / len(buf)))
+
+    def set_scale_factor(self, scale):
+        if not self.detected:
+            return None
+
+        fd = open(self.scale_path, 'w')
+        fd.write(str(int(round(scale))))
+        fd.close()
+        return
 
     def get_scale_factor(self):
         if not self.detected:
@@ -204,8 +173,46 @@
         time.sleep(self.light_delay)
 
 
+class ConnectionMonitor():
+    """A wrapper to monitor hardware plug/unplug events."""
+    def __init__(self):
+        self._monitoring = False
+
+    def start(self, subsystem, device_type=None, on_insert=None,
+              on_remove=None):
+        if self._monitoring:
+            raise Exception("Multiple start() call is not allowed")
+        self.on_insert = on_insert
+        self.on_remove = on_remove
+
+        # Setup the media monitor,
+        context = pyudev.Context()
+        self.monitor = pyudev.Monitor.from_netlink(context)
+        self.monitor.filter_by(subsystem, device_type)
+        self.monitor.start()
+        self._monitoring = True
+        self._watch_thread = threading.Thread(target=self.watch)
+        self._watch_end = threading.Event()
+        self._watch_thread.start()
+
+    def watch(self):
+        fd = self.monitor.fileno()
+        while not self._watch_end.isSet():
+            ret, _, _ = select.select([fd],[],[])
+            if fd in ret:
+                action, dev = self.monitor.receive_device()
+                if action == 'add' and self.on_insert:
+                    self.on_insert(dev.device_node)
+                elif action == 'remove' and self.on_remove:
+                    self.on_remove(dev.device_node)
+
+    def stop(self):
+        self._monitoring = False
+        self._watch_end.set()
+
+
 class factory_CameraPerformanceAls(test.test):
-    version = 1
+    version = 2
     preserve_srcdir = True
 
     # OpenCV will automatically search for a working camera device if we use
@@ -214,54 +221,72 @@
     _TEST_CHART_FILE = 'test_chart.png'
     _TEST_SAMPLE_FILE = 'sample.png'
 
-    # States for the state machine.
-    _STATE_INITIAL = -1
-    _STATE_WAIT_USB = 0
-    _STATE_PREPARE_MACHINE = 1
-    _STATE_ENTERING_SN = 2
-    _STATE_PREPARE_CAMERA = 3
-    _STATE_PREPARE_ALS = 4
-    _STATE_RESULT_TAB = 5
+    _PACKET_SIZE = 65000
 
     # Status in the final result tab.
-    _STATUS_NAMES = ['sn', 'cam_stat', 'cam_vc', 'cam_ls', 'cam_mtf',
+    _STATUS_NAMES = ['cam_stat', 'cam_vc', 'cam_ls', 'cam_mtf',
                      'als_stat', 'result']
-    _STATUS_LABELS = ['Serial Number',
-                      'Camera Functionality',
+    _STATUS_LABELS = ['Camera Functionality',
                       'Camera Visual Correctness',
                       'Camera Lens Shading',
                       'Camera Image Sharpness',
                       'ALS Functionality',
                       'Test Result']
+    _CAM_TESTS = ['cam_stat', 'cam_vc', 'cam_ls', 'cam_mtf']
+    _ALS_TESTS = ['als_stat']
 
     # LED patterns.
-    _LED_PREPARE_CAM_TEST = ((leds.LED_NUM, 0.25), (0, 0.25))
-    _LED_RUNNING_CAM_TEST = ((leds.LED_NUM, 0.05), (0, 0.05))
-    _LED_PREPARE_ALS_TEST = ((leds.LED_NUM|leds.LED_CAP, 0.25),
-                             (leds.LED_NUM, 0.25))
-    _LED_RUNNING_ALS_TEST = ((leds.LED_NUM|leds.LED_CAP, 0.05),
-                             (leds.LED_NUM, 0.05))
-    _LED_FINISHED_ALL_TEST = ((leds.LED_NUM|leds.LED_CAP, 0.25),
-                              (leds.LED_NUM|leds.LED_CAP, 0.25))
+    _LED_RUNNING_TEST = ((leds.LED_NUM|leds.LED_CAP, 0.05), (0, 0.05))
 
-    def advance_state(self):
-        if self.type == _TEST_TYPE_FULL:
-            self._state = self._state + 1
-            # Skip entering SN for full machine test.
-            if self._state == self._STATE_ENTERING_SN:
-                self._state = self._state + 1
-        else:
-            if self._state == self._STATE_RESULT_TAB:
-                self._state = self._STATE_PREPARE_MACHINE
+    # CSS style classes defined in the corresponding HTML file.
+    _STYLE_INFO = "color_idle"
+    _STYLE_PASS = "color_good"
+    _STYLE_FAIL = "color_bad"
+
+    def stylize(self, msg, style_type):
+        return '<span class="' + style_type + '">' + msg + '</span>'
+
+    def t_info(self, msg):
+        return self.stylize(msg, self._STYLE_INFO)
+
+    def t_pass(self, msg):
+        return self.stylize(msg, self._STYLE_PASS)
+
+    def t_fail(self, msg):
+        return self.stylize(msg, self._STYLE_FAIL)
+
+    def update_status(self, mid=None, msg=None):
+        message = ''
+        if msg:
+            message = msg
+        elif mid:
+            message = self.stylize(self.config['message'][mid],
+                                   self.config['msg_style'][mid])
+        self.ui.CallJSFunction("UpdateTestStatus", message)
+
+    def update_pbar(self, pid=None, value=None, add=True):
+        precent = 0
+        if value:
+            percent = value
+        elif pid:
+            all_time = self.config['chk_point'][self.type]
+            if add:
+                self.progress += self.config['chk_point'][pid]
             else:
-                self._state = self._state + 1
-        self.switch_widget(self._state_widget[self._state])
+                self.progress = self.config['chk_point'][pid]
+            percent = int(round((float(self.progress) / all_time) * 100))
+        self.ui.CallJSFunction("UpdatePrograssBar", '%d%%' % percent)
+
+    def register_events(self, events):
+        for event in events:
+            assert hasattr(self, event)
+            self.ui.AddEventHandler(event, getattr(self, event))
 
     def prepare_test(self):
         self.ref_data = camperf.PrepareTest(self._TEST_CHART_FILE)
 
     def on_usb_insert(self, dev_path):
-        if self._state == self._STATE_WAIT_USB:
+        if not self.config_loaded:
             # Initialize common test reference data.
             self.prepare_test()
             # Load config files and reset test results.
@@ -270,61 +295,19 @@
                 config_path = os.path.join(config_dir, 'camera.params')
                 self.config = self.base_config.Read(config_path)
                 self.reset_data()
-                self.advance_state()
-                factory.log("Config loaded.")
+                self.config_loaded = True
+                factory.console.info("Config loaded.")
+                self.ui.CallJSFunction("OnUSBInit", self.config['sn_format'])
+        else:
+            self.dev_path = dev_path
+            self.ui.CallJSFunction("OnUSBInsertion")
 
     def on_usb_remove(self, dev_path):
-        if self._state != self._STATE_WAIT_USB:
-            raise Exception("USB removal is not allowed during test")
+        if self.config_loaded:
+            factory.console.info("USB removal is not allowed during test!")
+            self.ui.CallJSFunction("OnUSBRemoval")
 
-    def register_callbacks(self, window):
-        def key_press_callback(widget, event):
-            if hasattr(self, 'last_widget'):
-                if hasattr(self.last_widget, 'key_callback'):
-                    return self.last_widget.key_callback(widget, event)
-            return False
-        window.connect('key-press-event', key_press_callback)
-        window.add_events(gtk.gdk.KEY_PRESS_MASK)
-
-    def switch_widget(self, widget_to_display):
-        if hasattr(self, 'last_widget'):
-            if widget_to_display is not self.last_widget:
-                self.last_widget.hide()
-                self.test_widget.remove(self.last_widget)
-            else:
-                return
-
-        self.last_widget = widget_to_display
-        self.test_widget.add(widget_to_display)
-        self.test_widget.show_all()
-
-    def on_sn_keypress(self, entry, key):
-        if key.keyval == gtk.keysyms.Tab:
-            entry.set_text(_TEST_SN_NUMBER)
-            return True
-        return False
-
-    def on_sn_complete(self, serial_number):
-        self.serial_number = serial_number
-        # TODO(itspeter): display the SN info in the result tab.
-        self._update_status('sn', self.check_sn_format(serial_number))
-        self.advance_state()
-
-    def check_sn_format(self, sn):
-        if re.search(self.config['sn_format'], sn):
-            return True
-        return False
-
-    def write_to_usb(self, filename, content, content_type=_CONTENT_TXT):
-        with MountedMedia(self.dev_path, 1) as mount_dir:
-            if content_type == _CONTENT_TXT:
-                with open(os.path.join(mount_dir, filename), 'w') as f:
-                    f.write(content)
-            elif content_type == _CONTENT_IMG:
-                cv2.imwrite(os.path.join(mount_dir, filename), content)
-        return True
-
-    def _setup_fixture(self):
+    def setup_fixture(self):
         '''Initialize the communication with the fixture.'''
         try:
             self.fixture = Fixture(self.config['fixture'])
@@ -341,7 +324,171 @@
         self.log('Test fixture successfully initialized.\n')
         return True
 
-    def _capture_low_noise_image(self, cam, n_samples):
+    def sync_fixture(self, event):
+        # Prevent multiple threads from running at the same time.
+        if hasattr(self, 'detecting_fixture') and self.detecting_fixture:
+            return
+        self.detecting_fixture = True
+        self.ui.CallJSFunction("OnDetectFixtureConnection")
+        cnt = 0
+        while not self.setup_fixture():
+            cnt += 1
+            if cnt >= self.config['fixture']['n_retry']:
+                self.detecting_fixture = False
+                self.ui.CallJSFunction("OnRemoveFixtureConnection")
+                return
+            time.sleep(self.config['fixture']['retry_delay'])
+        self.detecting_fixture = False
+        self.ui.CallJSFunction("OnAddFixtureConnection")
+
+    def on_u2s_insert(self, dev_path):
+        if self.config_loaded:
+            self.sync_fixture(None)
+
+    def on_u2s_remove(self, dev_path):
+        if self.config_loaded:
+            self.ui.CallJSFunction("OnRemoveFixtureConnection")
+
+    def update_result(self, row_name, result):
+        result_map = {
+            True: 'PASSED',
+            False: 'FAILED',
+            None: 'UNTESTED'
+        }
+        self.result_dict[row_name] = result_map[result]
+
+    def reset_data(self):
+        self.target = None
+        self.target_colorful = None
+        if self.type == _TEST_TYPE_FULL:
+            self.log = factory.console.info
+        else:
+            self.log_to_file = StringIO.StringIO()
+            self.log = lambda *x: (factory.console.info(*x),
+                                   self.log_to_file.write(*x))
+
+        for var in self.status_names:
+            self.update_result(var, None)
+        self.progress = 0
+        self.ui.CallJSFunction("ResetUiData", "")
+
+    def send_img_to_ui(self, data):
+        self.ui.CallJSFunction("ClearBuffer", "")
+        # Send the data in 64K packets due to the socket packet size limit.
+        data_len = len(data)
+        p = 0
+        while p < data_len:
+            if p + self._PACKET_SIZE > data_len:
+                self.ui.CallJSFunction("AddBuffer", data[p:data_len-1])
+                p = data_len
+            else:
+                self.ui.CallJSFunction("AddBuffer",
+                                         data[p:p+self._PACKET_SIZE])
+                p += self._PACKET_SIZE
+
+    def update_preview(self, img, container_id, scale=0.5):
+        # Encode the image in the JPEG format.
+        preview = cv2.resize(img, None, fx=scale, fy=scale,
+                         interpolation=cv2.INTER_AREA)
+        cv2.imwrite('temp.jpg', preview)
+        with open('temp.jpg', 'r') as fd:
+            img_data = base64.b64encode(fd.read()) + "="
+
+        # Update the preview screen with javascript.
+        self.send_img_to_ui(img_data)
+        self.ui.CallJSFunction("UpdateImage", container_id)
+        return
+
+    def compile_result(self, test_list, use_untest=True):
+        ret = self.result_dict
+        if all('PASSED' == ret[x] for x in test_list):
+            return True
+        if use_untest and any('UNTESTED' == ret[x] for x in test_list):
+            return None
+        return False
+
+    def generate_final_result(self):
+        self.update_status(mid='end_test')
+        self.cam_pass = self.compile_result(self._CAM_TESTS)
+        self.als_pass = self.compile_result(self._ALS_TESTS)
+        result = self.compile_result(self.status_names[:-1], use_untest=False)
+        self.update_result('result', result)
+        self.log("Result in summary:\n%s\n" %
+                 pprint.pformat(self.result_dict))
+        self.update_pbar(pid='end_test')
+
+    def write_to_usb(self, filename, content, content_type=_CONTENT_TXT):
+        with MountedMedia(self.dev_path, 1) as mount_dir:
+            if content_type == _CONTENT_TXT:
+                with open(os.path.join(mount_dir, filename), 'a') as f:
+                    f.write(content)
+            elif content_type == _CONTENT_IMG:
+                cv2.imwrite(os.path.join(mount_dir, filename), content)
+        return True
+
+    def save_log_to_usb(self):
+        # Save an image for further analysis in case of the camera
+        # performance fail.
+        self.update_status(mid='save_to_usb')
+        if (not self.cam_pass) and (self.target is not None):
+            if not self.write_to_usb(self.serial_number + ".bmp",
+                                     self.target, _CONTENT_IMG):
+                return False
+        return self.write_to_usb(
+            self.serial_number + ".txt", self.log_to_file.getvalue())
+
+    def finalize_test(self):
+        self.generate_final_result()
+        if self.type == _TEST_TYPE_AB:
+            # We block the test flow until we successfully dumped the result.
+            while not self.save_log_to_usb():
+                time.sleep(0.5)
+            self.update_pbar(pid='save_to_usb')
+
+        # Display final result.
+        def get_str(ret, prefix, use_untest=True):
+            if ret:
+                return self.t_pass(prefix + 'PASS')
+            if use_untest and (ret is None):
+                return self.t_fail(prefix + 'UNFINISHED')
+            return self.t_fail(prefix + 'FAIL')
+        cam_result = get_str(self.cam_pass, 'Camera: ', False)
+        als_result = get_str(self.als_pass, 'ALS: ')
+        self.update_status(msg=cam_result + '<br>' + als_result)
+        self.update_pbar(value=100)
+
+    def exit_test(self, event):
+        factory.log('%s run_once finished' % self.__class__)
+        if self.result_dict['result'] == 'PASSED':
+            self.ui.Pass()
+        else:
+            self.ui.Fail('Camera/ALS test failed.')
+
+    def run_test(self, event=None):
+        self.reset_data()
+        self.update_status(mid='start_test')
+        if self.type == _TEST_TYPE_AB:
+            self.serial_number = event.data.get('sn', '')
+        if not self.setup_fixture():
+            self.update_status(mid='fixture_fail')
+            self.ui.CallJSFunction("OnRemoveFixtureConnection")
+            return
+        self.update_pbar(pid='start_test')
+
+        if self.type == _TEST_TYPE_FULL:
+            with leds.Blinker(self._LED_RUNNING_TEST):
+                self.test_camera_performance()
+                self.update_pbar(pid='cam_finish', add=False)
+                self.test_als_calibration()
+        else:
+            self.test_camera_performance()
+            self.update_pbar(pid='cam_finish', add=False)
+            self.test_als_calibration()
+        self.update_pbar(pid='als_finish' + self.type, add=False)
+
+        self.finalize_test()
+
+    def capture_low_noise_image(self, cam, n_samples):
         '''Capture a sequence of images and average them to reduce noise.'''
         if n_samples < 1:
             n_samples = 1
@@ -353,66 +500,81 @@
         img /= n_samples
         return img.round().astype(np.uint8)
 
-    def _test_camera_functionality(self):
+    def test_camera_functionality(self):
         # Initialize the camera with OpenCV.
+        self.update_status(mid='init_cam')
         cam = cv2.VideoCapture(self._DEVICE_INDEX)
         if not cam.isOpened():
             cam.release()
-            self._update_status('cam_stat', False)
+            self.update_result('cam_stat', False)
             self.log('Failed to initialize the camera. '
                      'Could be bad module, bad connection or '
-                     'insufficient USB bandwidth.\n')
+                     'bad USB initialization.\n')
             return False
+        self.update_pbar(pid='init_cam')
 
         # Set resolution.
+        self.update_status(mid='set_cam_res')
         conf = self.config['cam_stat']
         cam.set(cv.CV_CAP_PROP_FRAME_WIDTH, conf['img_width'])
         cam.set(cv.CV_CAP_PROP_FRAME_HEIGHT, conf['img_height'])
         if (conf['img_width'] != cam.get(cv.CV_CAP_PROP_FRAME_WIDTH) or
             conf['img_height'] != cam.get(cv.CV_CAP_PROP_FRAME_HEIGHT)):
             cam.release()
-            self._update_status('cam_stat', False)
+            self.update_result('cam_stat', False)
             self.log("Can't set the image size. "
-                     "Possibly caused by insufficient USB bandwidth.\n")
+                     "Possibly caused by bad USB initialization.\n")
             return False
+        self.update_pbar(pid='set_cam_res')
 
         # Try reading an image from the camera.
+        self.update_status(mid='try_read_cam')
         success, _ = cam.read()
         if not success:
             cam.release()
-            self._update_status('cam_stat', False)
+            self.update_result('cam_stat', False)
             self.log("Failed to capture an image with the camera.\n")
             return False
+        self.update_pbar(pid='try_read_cam')
 
         # Let the camera's auto-exposure algorithm adjust to the fixture
         # lighting condition.
-        start = time.clock()
-        while time.clock() - start < conf['buf_time']:
+        self.update_status(mid='wait_cam_awb')
+        start = time.time()
+        while time.time() - start < conf['buf_time']:
             _, _ = cam.read()
+        self.update_pbar(pid='wait_cam_awb')
 
         # Read the image that we will use.
+        self.update_status(mid='record_img')
         n_samples = conf['n_samples']
-        img = self._capture_low_noise_image(cam, n_samples)
-        self.target = cv2.cvtColor(img, cv.CV_BGR2GRAY)
-        self._update_status('cam_stat', True)
-        self.log('Successfully captured an image.\n')
-
-        # Use the sample image in the unit-test mode.
+        self.target_colorful = self.capture_low_noise_image(cam, n_samples)
         if self.unit_test:
-            self.target = cv2.imread(self._TEST_SAMPLE_FILE,
-                                     cv.CV_LOAD_IMAGE_GRAYSCALE)
+            self.target_colorful = cv2.imread(self._TEST_SAMPLE_FILE)
+
+        self.target = cv2.cvtColor(self.target_colorful, cv.CV_BGR2GRAY)
+        self.update_result('cam_stat', True)
+        self.log('Successfully captured a sample image.\n')
+        self.update_preview(self.target_colorful, "camera_image",
+                            scale=self.config['preview']['scale'])
         cam.release()
+        self.update_pbar(pid='record_img')
         return True
 
-    def _test_camera_core(self):
-        if not self._test_camera_functionality():
+    def test_camera_performance(self):
+        if not self.test_camera_functionality():
             return
 
         # Check the captured test pattern image validity.
+        self.update_status(mid='check_vc')
         success, tar_data = camperf.CheckVisualCorrectness(
             self.target, self.ref_data, **self.config['cam_vc'])
+        analyzed = self.target_colorful.copy()
+        renderer.DrawVC(analyzed, success, tar_data)
+        self.update_preview(analyzed, "analyzed_image",
+                            scale=self.config['preview']['scale'])
 
-        self._update_status('cam_vc', success)
+        self.update_result('cam_vc', success)
         if hasattr(tar_data, 'shift'):
             self.log('Image shift percentage: %f\n' % tar_data.shift)
             self.log('Image tilt: %f degrees\n' % tar_data.tilt)
@@ -425,188 +587,139 @@
                          tar_data.edges.shape[0])
             self.log('Visual correctness: %s\n' % tar_data.msg)
             return
+        self.update_pbar(pid='check_vc')
 
         # Check if the lens shading is present.
+        self.update_status(mid='check_ls')
         success, tar_ls = camperf.CheckLensShading(
             self.target, **self.config['cam_ls'])
 
-        self._update_status('cam_ls', success)
+        self.update_result('cam_ls', success)
         if tar_ls.check_low_freq:
             self.log('Low-frequency response value: %f\n' %
                                    tar_ls.response)
         if not success:
             self.log('Lens shading: %s\n' % tar_ls.msg)
             return
+        self.update_pbar(pid='check_ls')
 
         # Check the image sharpness.
+        self.update_status(mid='check_mtf')
         success, tar_mtf = camperf.CheckSharpness(
             self.target, tar_data.edges, **self.config['cam_mtf'])
 
-        self._update_status('cam_mtf', success)
+        self.update_result('cam_mtf', success)
         self.log('MTF value: %f\n' % tar_mtf.mtf)
         if hasattr(tar_mtf, 'min_mtf'):
             self.log('Lowest MTF value: %f\n' % tar_mtf.min_mtf)
         if not success:
             self.log('Sharpness: %s\n' % tar_mtf.msg)
+        self.update_pbar(pid='check_mtf')
         return
 
-    def _test_als_core(self):
+    def test_als_write_vpd(self, calib_result):
+        self.update_status(mid='dump_to_vpd')
+        conf = self.config['als']
+        if not calib_result:
+            self.update_result('als_stat', False)
+            self.log('ALS calibration data is incorrect.\n')
+            return False
+        if subprocess.call(conf['save_vpd'] % calib_result, shell=True):
+            self.update_result('als_stat', False)
+            self.log('Writing VPD data failed!\n')
+            return False
+        self.log('Successfully calibrated ALS scales.\n')
+        self.update_pbar(pid='dump_to_vpd')
+        return True
+
+    def test_als_switch_to_next_light(self):
+        self.update_status(mid='adjust_light')
+        conf = self.config['als']
+        self.light_state += 1
+        self.fixture.set_light(self.light_state)
+        self.update_pbar(pid='adjust_light')
+        if not self.unit_test:
+            self.fixture.assert_success()
+        if self.light_state >= len(conf['luxs']):
+            return False
+        self.update_status(mid='wait_fixture')
+        self.fixture.wait_for_light_switch()
+        self.update_pbar(pid='wait_fixture')
+        return True
+
+    def test_als_calibration(self):
         # Initialize the ALS.
+        self.update_status(mid='init_als')
         conf = self.config['als']
         self.als = ALS(val_path=conf['val_path'],
                        scale_path=conf['scale_path'])
         if not self.als.detected:
-            self._update_status('als_stat', False)
+            self.update_result('als_stat', False)
             self.log('Failed to initialize the ALS.\n')
             return
+        self.als.set_scale_factor(conf['calibscale'])
+        self.update_pbar(pid='init_als')
 
         # Go through all different lighting settings
         # and record ALS values.
+        calib_result = 0
         try:
+            vals = []
             while True:
                 # Get ALS values.
+                self.update_status(mid='read_als%d' % self.light_state)
                 scale = self.als.get_scale_factor()
-                val = self.als.read_mean(samples=5, delay=0)
+                val = self.als.read_mean(samples=conf['n_samples'],
+                                         delay=conf['read_delay'])
+                vals.append(val)
                 self.log('Lighting preset lux value: %d\n' %
                          conf['luxs'][self.light_state])
                 self.log('ALS value: %d\n' % val)
                 self.log('ALS calibration scale: %d\n' % scale)
+                # Check if it is a false read.
+                if not val:
+                    self.update_result('als_stat', False)
+                    self.log('The ALS value is stuck at zero.\n')
+                    return
+                # Compute calibration data if it is the calibration target.
+                if conf['luxs'][self.light_state] == conf['calib_lux']:
+                    calib_result = int(round(float(conf['calib_target']) /
+                                             val * scale))
+                    self.log('ALS calibration data will be %d\n' %
+                             calib_result)
+                self.update_pbar(pid='read_als%d' % self.light_state)
 
                 # Go to the next lighting preset.
-                self.light_state += 1
-                self.fixture.set_light(self.light_state)
-                if not self.unit_test:
-                    self.fixture.assert_success()
-                if self.light_state >= len(conf['luxs']):
+                if not self.test_als_switch_to_next_light():
                     break
-                self.fixture.wait_for_light_switch()
+
+            # Check value ordering.
+            for i, li in enumerate(conf['luxs']):
+                for j in range(i):
+                    if ((li > conf['luxs'][j] and vals[j] >= vals[i]) or
+                        (li < conf['luxs'][j] and vals[j] <= vals[i])):
+                        self.update_result('als_stat', False)
+                        self.log('The ordering of ALS values is wrong.\n')
+                        return
         except (FixtureException, serial.serialutil.SerialException) as e:
             self.fixture = None
-            self._update_status('als_stat', None)
+            self.update_result('als_stat', None)
             self.log("The test fixture was disconnected!\n")
+            self.ui.CallJSFunction("OnRemoveFixtureConnection")
             return
         except:
-            self._update_status('als_stat', False)
-            self.log('Failed to read values from ALS.\n')
+            self.update_result('als_stat', False)
+            self.log('Failed to read values from ALS or unknown error.\n')
             return
-        self._update_status('als_stat', True)
         self.log('Successfully recorded ALS values.\n')
+
+        # Save ALS values to vpd for FATP test.
+        if self.type == _TEST_TYPE_FULL:
+            if not self.test_als_write_vpd(calib_result):
+                return
+        self.update_result('als_stat', True)
         return
 
-    def test_camera(self, skip_flag):
-        if self.type == _TEST_TYPE_FULL:
-            self.blinker.Stop()
-
-            if not skip_flag:
-                with leds.Blinker(self._LED_RUNNING_CAM_TEST):
-                    self._test_camera_core()
-
-            self.blinker = leds.Blinker(self._LED_PREPARE_ALS_TEST)
-            self.blinker.Start()
-        else:
-            if not skip_flag:
-                self._test_camera_core()
-
-        self.advance_state()
-
-    def test_als(self, skip_flag):
-        if self.type == _TEST_TYPE_FULL:
-            self.blinker.Stop()
-
-            if not skip_flag:
-                with leds.Blinker(self._LED_RUNNING_ALS_TEST):
-                    self._test_als_core()
-
-            self.blinker = leds.Blinker(self._LED_FINISHED_ALL_TEST)
-            self.blinker.Start()
-        else:
-            if not skip_flag:
-                self._test_als_core()
-
-        self.generate_final_result()
-        self.advance_state()
-
-    def _update_status(self, row_name, result):
-        """Updates status in display_dict."""
-        result_map = {
-            True: ful.PASSED,
-            False: ful.FAILED,
-            None: ful.UNTESTED
-        }
-        assert result in result_map, "Unknown result"
-        self.display_dict[row_name]['status'] = result_map[result]
-
-    def generate_final_result(self):
-        self._result = all(
-           ful.PASSED == self.display_dict[var]['status']
-           for var in self.status_names[:-1])
-        self._update_status('result', self._result)
-        self.log("Result in summary:\n%s\n" %
-                 pprint.pformat(self.display_dict))
-
-    def save_log(self):
-        # Save an image for further analysis in case of the camera
-        # performance fail.
-        cam_perf_pass = all(ful.PASSED == self.display_dict[var]['status']
-                            for var in ['cam_vc', 'cam_ls', 'cam_mtf'])
-        if (not cam_perf_pass) and (self.target is not None):
-            if not self.write_to_usb(self.serial_number + ".bmp",
-                                     self.target, _CONTENT_IMG):
-                return False
-        return self.write_to_usb(
-            self.serial_number + ".txt", self.log_to_file.getvalue())
-
-    def reset_data(self):
-        self.target = None
-        if self.type == _TEST_TYPE_FULL:
-            self.log = logging.info
-        else:
-            self.log_to_file = StringIO.StringIO()
-            self.sn_input_widget.get_entry().set_text('')
-            self.log = self.log_to_file.write
-
-        for var in self.status_names:
-            self._update_status(var, None)
-
-    def on_result_enter(self):
-        if self.type == _TEST_TYPE_FULL:
-            self.blinker.Stop()
-            gtk.main_quit()
-        else:
-            # The UI will stop in this screen unless log is saved.
-            if self.save_log():
-                self.reset_data()
-                self.advance_state()
-        return False
-
-    def on_close_prepare_machine(self):
-        if self.type == _TEST_TYPE_FULL:
-            self.blinker = leds.Blinker(self._LED_PREPARE_CAM_TEST)
-            self.blinker.Start()
-        # Try to setup the fixture. This step blocks until we can find the
-        # fixture successfully.
-        if not self._setup_fixture():
-            return False
-        self.advance_state()
-        return True
-
-    def make_result_widget(self, on_key_enter):
-        widget = gtk.VBox()
-        widget.add(ful.make_label(_MESSAGE_RESULT_TAB_FULL
-                                  if self.type == _TEST_TYPE_FULL
-                                  else _MESSAGE_RESULT_TAB_ABONLY))
-
-        for name, label in zip(self.status_names, self.status_labels):
-            td, tw = ful.make_status_row(label, ful.UNTESTED, _LABEL_SIZE)
-            self.display_dict[name] = td
-            widget.add(tw)
-        def key_press_callback(widget, event):
-            if event.keyval == gtk.keysyms.Return:
-                on_key_enter()
-
-        widget.key_callback = key_press_callback
-        return widget
-
     def run_once(self, test_type=_TEST_TYPE_FULL, unit_test=False):
         '''The entry point of the test.
 
@@ -629,77 +742,30 @@
         '''
         factory.log('%s run_once' % self.__class__)
 
-        # Initialize variables.
+        # Initialize variables and environment.
         assert test_type in [_TEST_TYPE_FULL, _TEST_TYPE_AB]
         assert unit_test in [True, False]
         self.type = test_type
         self.unit_test = unit_test
-        self.display_dict = {}
+        self.config_loaded = False
+        self.status_names = self._STATUS_NAMES
+        self.status_labels = self._STATUS_LABELS
+        self.result_dict = {}
         self.base_config = PluggableConfig({})
-        self.last_handler = None
         os.chdir(self.srcdir)
-        if self.type == _TEST_TYPE_FULL:
-            self.status_names = self._STATUS_NAMES[1:]
-            self.status_labels = self._STATUS_LABELS[1:]
-        else:
-            self.status_names = self._STATUS_NAMES
-            self.status_labels = self._STATUS_LABELS
 
-        # Set up the UI widgets.
-        self.usb_prompt_widget = gtk.VBox()
-        self.usb_prompt_widget.add(ful.make_label(_MESSAGE_USB))
-        self.prepare_machine_widget = make_prepare_widget(
-            (_MESSAGE_PREPARE_MACHINE if self.type == _TEST_TYPE_FULL
-             else _MESSAGE_PREPARE_PANEL),
-            self.on_close_prepare_machine)
-        self.prepare_camera_widget = make_prepare_widget(
-                _MESSAGE_PREPARE_CAMERA,
-                lambda : self.test_camera(skip_flag=False),
-                lambda : self.test_camera(skip_flag=True))
-        self.prepare_als_widget = make_prepare_widget(
-                _MESSAGE_PREPARE_ALS,
-                lambda : self.test_als(skip_flag=False),
-                lambda : self.test_als(skip_flag=True))
-        self.result_widget = self.make_result_widget(self.on_result_enter)
+        # Setup the usb disk and usb-to-serial adapter monitor.
+        usb_monitor = ConnectionMonitor()
+        usb_monitor.start(subsystem='block', device_type='disk',
+                          on_insert=self.on_usb_insert,
+                          on_remove=self.on_usb_remove)
+        u2s_monitor = ConnectionMonitor()
+        u2s_monitor.start(subsystem='usb-serial',
+                          on_insert=self.on_u2s_insert,
+                          on_remove=self.on_u2s_remove)
 
-        self.sn_input_widget = ful.make_input_window(
-            prompt='Enter Serial Number (TAB to use testing sample SN):',
-            on_validate=self.check_sn_format,
-            on_keypress=self.on_sn_keypress,
-            on_complete=self.on_sn_complete)
-
-        # Make sure the entry in widget will have focus.
-        self.sn_input_widget.connect(
-            "show",
-            lambda *x : self.sn_input_widget.get_entry().grab_focus())
-
-        # Setup the relation of states and widgets.
-        self._state_widget = {
-            self._STATE_INITIAL: None,
-            self._STATE_WAIT_USB: self.usb_prompt_widget,
-            self._STATE_PREPARE_MACHINE: self.prepare_machine_widget,
-            self._STATE_ENTERING_SN: self.sn_input_widget,
-            self._STATE_PREPARE_CAMERA: self.prepare_camera_widget,
-            self._STATE_PREPARE_ALS: self.prepare_als_widget,
-            self._STATE_RESULT_TAB: self.result_widget
-        }
-
-        # Setup the usb monitor,
-        monitor = MediaMonitor()
-        monitor.start(on_insert=self.on_usb_insert,
-                      on_remove=self.on_usb_remove)
-
-        # Setup the initial display.
-        self.test_widget = gtk.VBox()
-        self._state = self._STATE_INITIAL
-        self.advance_state()
-        ful.run_test_widget(
-                self.job,
-                self.test_widget,
-                window_registration_callback=self.register_callbacks)
-
-        if not self._result:
-            raise error.TestFail('Camera/ALS test failed by user indication\n' +
-                                 '品管人員懷疑故障,請檢修')
-
-        factory.log('%s run_once finished' % self.__class__)
+        # Startup the UI.
+        self.ui = UI()
+        self.register_events(['sync_fixture', 'exit_test', 'run_test'])
+        self.ui.CallJSFunction("InitLayout", self.type == _TEST_TYPE_FULL)
+        self.ui.Run()
diff --git a/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.html b/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.html
new file mode 100755
index 0000000..9023a1a
--- /dev/null
+++ b/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.html
@@ -0,0 +1,365 @@
+<html>
+<head>
+<meta charset="UTF-8">
+<style type="text/css">
+
+.button {
+	display: inline-block;
+	vertical-align: baseline;
+	margin: 0 2px;
+	outline: none;
+	cursor: pointer;
+	text-align: center;
+	text-decoration: none;
+	padding: .5em 2em .55em;
+	text-shadow: 2px 2px 4px rgba(0,0,0,.5);
+	-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2);
+}
+.button:hover {
+	text-decoration: none;
+}
+
+.bigrounded {
+	-webkit-border-radius: 2em;
+}
+
+.title_box {
+    background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#000));
+}
+
+.panel_box{
+	color: #e9e9e9;
+	background: -webkit-gradient(linear, left top, left bottom, from(#888), to(#575757));
+}
+
+.panel_good{
+	background: -webkit-gradient(linear, left top, left bottom, from(#7db72f), to(#4e7d0e));
+}
+
+.panel_bad{
+	background: -webkit-gradient(linear, left top, left bottom, from(#ed1c24), to(#aa1317));
+}
+
+.text_shadow {
+    text-shadow: 4px 4px 6px #333333;
+}
+
+.text_shadow_white {
+    text-shadow: 1px 1px 3px #E0E0E0;
+}
+
+.text_shadow_small {
+    text-shadow: 2px 2px 3px #333333;
+}
+
+.color_good {
+    color:#7db72f;
+}
+
+.color_bad {
+    color:#c9151b;
+}
+
+.color_idle {
+    color:LightSkyBlue;
+}
+
+.font_title {
+	font-family:Arial;
+	font-size:150%;
+	font-weight:bold;
+	color:white;
+}
+
+.font_label {
+	font-family:Arial;
+	font-size:110%;
+}
+
+.font_input {
+	font-family:Monospace;
+	font-size:150%;
+	font-weight:bold;
+}
+
+.font_status {
+	font-family:Monospace;
+	font-size:250%;
+	font-weight:bold;
+}
+
+.input_box {
+	text-align: center;
+	width: 100%;
+	height: 100%;
+	border: none;
+	color: -webkit-text;
+	background: -webkit-gradient(linear, left top, left bottom, from(#7db72f), to(#4e7d0e));
+	font-family:Monospace;
+	font-size:100%;
+	font-weight:bold;
+}
+
+.input_box:invalid {
+	background: -webkit-gradient(linear, left top, left bottom, from(#ed1c24), to(#aa1317));
+}
+
+.input_box:valid {
+	background: -webkit-gradient(linear, left top, left bottom, from(#7db72f), to(#4e7d0e));
+}
+
+/* color styles  */
+/* black */
+.black {
+	color: #d7d7d7;
+	border:none;
+	background: -webkit-gradient(linear, left top, left bottom, from(#666), to(#222));
+	-webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
+}
+.black:hover {
+	background: -webkit-gradient(linear, left top, left bottom, from(#444), to(#222));
+}
+.black:active {
+	color: #666;
+	background: -webkit-gradient(linear, left top, left bottom, from(#222), to(#444));
+}
+
+/* gray */
+.gray {
+	color: #e9e9e9;
+	background: -webkit-gradient(linear, left top, left bottom, from(#888), to(#575757));
+}
+/*
+.gray:hover {
+	background: #616161;
+	background: -webkit-gradient(linear, left top, left bottom, from(#757575), to(#4b4b4b));
+}
+.gray:active {
+	color: #afafaf;
+	background: -webkit-gradient(linear, left top, left bottom, from(#575757), to(#888));
+}
+*/
+/* white */
+.white {
+	color: #606060;
+	background:-webkit-gradient(linear, left top, left bottom, from(#fff), to(#dcdcdc));
+}
+/*
+.white:hover {
+	background: #ededed;
+	background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#dcdcdc));
+}
+.white:active {
+	color: #999;
+	background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#fff));
+}
+*/
+
+/* main image container */
+.popup_img{
+	position: relative;
+	z-index: 1;
+}
+
+/* CSS for image within container */
+.popup_img img.original_img{
+	position: relative;
+	z-index: 3;
+	opacity: 1;
+	-webkit-transition: all 0.5s ease-in-out;
+}
+
+/* CSS for image when mouse hovers over main container */
+.popup_img:hover img.original_img{
+	-webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+	-webkit-transform: scale(1.20, 1.20);
+	opacity: 0;
+	/*left: -15%;*/
+}
+
+/* CSS for desc div of each image. */
+.popup_img div.desc{
+	position: absolute;
+	width: 20%;
+	 /* Set z-index to that less than image's, so it's hidden beneath it */
+	z-index: 1;
+	padding: 8px;
+	background: rgba(0, 0, 0, 0.8); /* black bg with 80% opacity */
+	color: white;
+	-webkit-border-radius: 0 0 8px 8px;
+	opacity: 0; /* Set initial opacity to 0 */
+	-webkit-box-shadow: 0 0 6px rgba(0, 0, 0, 0.8);
+	-webkit-transition: all 0.5s ease 0.1s;
+}
+
+.popup_img div.desc a{
+	color: white;
+}
+
+/* CSS for desc div when mouse hovers over main container */
+.popup_img:hover div.desc{
+	opacity:1; /* Reveal desc DIV fully */
+}
+
+.popup_img div.rightslide{
+	width:26%;
+	height:84%;
+	top:7%;
+	right:10%;
+	padding-left:25px;
+	-webkit-border-radius: 0 8px 8px 0;
+}
+
+.popup_img:hover div.rightslide{
+	-webkit-transform: translate(110%, 0%);
+}
+
+.popup_img img.analyzed_img{
+	position:absolute;
+	margin: auto;
+	top:0%;
+	z-index: 2;
+	opacity: 1;
+	-webkit-transition: all 0.5s ease-in-out;
+}
+
+.popup_img:hover img.analyzed_img{
+	-webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+	-webkit-transform: scale(1.20, 1.20);
+	/*left: -15%;*/
+}
+
+.progress div.text{
+	position: relative;
+	z-index: 2;
+}
+
+.progress div.bar_holder{
+	position:absolute;
+	width:98.5%;
+	z-index:1;
+	height:2.5%;
+	top:94.5%;
+	left:0%;
+	right:0%;
+	bottom:0%;
+	margin:auto;
+}
+
+.progress div.bar{
+	position: relative;
+	z-index: 1;
+	width: 0%;
+	height: 100%;
+	-webkit-border-radius: 5px;
+	-webkit-box-shadow: 0 1px 5px #000 inset, 0 1px 0 #444;
+	-webkit-transition: width .4s ease-in-out;
+}
+
+.progress div.bar span{
+	display: inline-block;
+	height: 100%;
+	-webkit-border-radius: 3px;
+	-webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset;
+    -webkit-transition: width .4s ease-in-out;
+}
+
+.progress div.pgray span {
+	background-color: #999;
+}
+
+.progress div.stripes span {
+	-webkit-background-size: 30px 30px;
+	-webkit-box-shadow: -2px -2px 10px rgba(0, 0, 0, .7) inset;
+	background-image: -webkit-gradient(linear, left top, right bottom,
+	                  color-stop(.25, rgba(255, 255, 255, .15)), color-stop(.25, transparent),
+	                  color-stop(.5, transparent), color-stop(.5, rgba(255, 255, 255, .15)),
+	                  color-stop(.75, rgba(255, 255, 255, .15)), color-stop(.75, transparent),
+	                  to(transparent));
+	background-image: -webkit-linear-gradient(135deg, rgba(255, 255, 255, .15) 25%, transparent 25%,
+	                  transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%,
+	                  transparent 75%, transparent);
+	-webkit-animation: animate-stripes 3s linear infinite;
+}
+
+@-webkit-keyframes animate-stripes {
+	0% {background-position: 0 0;} 100% {background-position: 60px 0;}
+}
+
+</style>
+</head>
+<body>
+<div id='prompt_usb' class="panel_box text_shadow" style="height:100%;clear:both;text-align:center">
+<table class="font_status" style="margin:auto;height:100%;"><tr><td style="vertical-align:middle;text-align:center;">
+<div class="text">
+<span class="color_idle">Please insert the USB stick to load test parameters.<br>請插入U盤讀取測試參數</span>
+</div>
+</td></tr></table>
+</div>
+<div id="container" hidden>
+<div id="Title" class="title_box" style="height:5%;">
+<table class="text_shadow_white font_title" style="margin:auto;height:100%;"><tr><td style="vertical-align:middle">
+Camera Performance Test and ALS Calibration
+</td></tr></table>
+</div>
+<div id="menu" class="panel_box" style="height:75%;width:20%;float:left;">
+<div class="panel_box text_shadow_small font_label" style="height:5%;padding-left:3%">Params(測試參數):</div>
+<div id="test_param" class="panel_bad" style="height:10%;text-align:center">
+<table class="font_input" style="margin:auto;height:100%;"><tr><td style="vertical-align:middle">
+<div id="usb_status">
+UNLOADED
+</div>
+</td></tr></table>
+</div>
+<div id="test_sn_label" class="panel_box text_shadow_small font_label" style="height:5%;padding-left:3%">SN(序號):</div>
+<div id="test_sn" class="panel_good" style="height:10%;text-align:center;">
+<table class="font_input" style="margin:auto;height:100%;border:0;border-spacing:0" cellpadding="0" cellspacing="0">
+<tr><td style="vertical-align:middle">
+<input id="serial_number" class="input_box" disabled type="text" value="" required pattern="TEST_SN_NUMBER" onclick="OnSnInputBoxClick();" onkeypress="UpdateTestBottonStatus();">
+</td></tr></table>
+</div>
+<div class="panel_box text_shadow_small font_label" style="height:5%;padding-left:3%">Fixture(製具連結):</div>
+<div id="test_fixture" class="panel_bad" style="height:10%;text-align:center">
+<table class="font_input" style="margin:auto;height:100%;"><tr><td style="vertical-align:middle">
+<div id="fixture_status">
+UNAVAILABLE
+</div>
+</td></tr></table>
+</div>
+<div id="menu_placeholder" style="height:15%">
+</div>
+<div style="height:20%;text-align:center">
+<button id="btn_run_test" class="button black" disabled type="button" onclick="BottonRunTestClick();" style="width:100%;height:97%;font-size:140%">Run Test<br>開始測試</button>
+</div>
+<div style="height:20%;text-align:center">
+<button id="btn_exit_test" class="button black" type="button" onclick="BottonExitTestClick();" style="width:100%;height:97%;font-size:140%">Exit Test<br>離開測試</button>
+</div>
+</div>
+<div id="content" class="white" style="height:75%;width:80%;float:right;">
+<table style="margin:auto;height:100%;"><tr><td style="vertical-align:middle">
+<div class="popup_img" style="">
+<img id="camera_image" class="original_img" hidden src="" alt="Camera Image"></img>
+<div class="desc rightslide" hidden>
+</div>
+<img id="analyzed_image" class="analyzed_img" hidden src=""></img>
+</div>
+</td></tr></table>
+</div>
+<div id="test_console" class="panel_box text_shadow" style="height:20%;clear:both;text-align:center">
+<table class="font_status" style="margin:auto;height:100%;"><tr><td style="vertical-align:middle;text-align:center;">
+<div class="progress">
+<div id="test_status" class="text">
+<span class="color_idle">IDLE</span>
+</div>
+<div class="bar_holder">
+<div id="progress_bar" class="bar pgray stripes">
+<span style="width: 100%"></span>
+</div>
+</div>
+</div>
+</div>
+</td></tr></table>
+</div>
+</div>
+</div>
+</body></html>
diff --git a/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.js b/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.js
new file mode 100755
index 0000000..f4e4da7
--- /dev/null
+++ b/client/site_tests/factory_CameraPerformanceAls/static/factory_CameraPerformanceAls.js
@@ -0,0 +1,125 @@
+window.onkeydown = function(event) {
+    if (event.keyCode == 13 || event.keyCode == 32) {
+        var testButton = document.getElementById("btn_run_test");
+        if (!testButton.disabled)
+            BottonRunTestClick();
+    }
+}
+
+function InitLayout(testFull) {
+    if (testFull) {
+        document.getElementById("test_sn_label").hidden = true;
+        document.getElementById("test_sn").hidden = true;
+        document.getElementById("menu_placeholder").style.height = "30%";
+    } else {
+        var snInputBox = document.getElementById("serial_number");
+        snInputBox.disabled = false;
+        snInputBox.autofocus = true;
+    }
+}
+
+function UpdateTestBottonStatus() {
+    var testButton = document.getElementById("btn_run_test");
+    testButton.disabled =
+        !(document.getElementById('serial_number').validity.valid &&
+        (document.getElementById('usb_status').innerHTML == 'LOADED') &&
+        (document.getElementById('fixture_status').innerHTML == 'OK'));
+}
+
+function OnSnInputBoxClick() {
+    var snInputBox = document.getElementById("serial_number");
+    snInputBox.value="";
+}
+
+function OnUSBInsertion() {
+    document.getElementById("usb_status").innerHTML = "LOADED";
+    document.getElementById("test_param").className = "panel_good";
+    UpdateTestBottonStatus();
+}
+
+function OnUSBInit(pattern) {
+    document.getElementById("prompt_usb").hidden = true;
+    document.getElementById("container").hidden = false;
+    document.getElementById("usb_status").innerHTML = "LOADED";
+    document.getElementById("test_param").className = "panel_good";
+    ConfigureSNInputbox(pattern);
+    test.sendTestEvent('sync_fixture', {});
+}
+
+function OnUSBRemoval() {
+    document.getElementById("usb_status").innerHTML = "UNLOADED";
+    document.getElementById("test_param").className = "panel_bad";
+    UpdateTestBottonStatus();
+}
+
+function ConfigureSNInputbox(pattern) {
+    var snInputBox = document.getElementById("serial_number");
+    if (!snInputBox.disabled) {
+        snInputBox.pattern = pattern;
+        snInputBox.focus();
+    }
+}
+
+function OnAddFixtureConnection() {
+    document.getElementById("fixture_status").innerHTML = "OK";
+    document.getElementById("test_fixture").className = "panel_good";
+    UpdateTestBottonStatus();
+}
+
+function OnRemoveFixtureConnection() {
+    document.getElementById("fixture_status").innerHTML = "UNAVAILABLE";
+    document.getElementById("test_fixture").className = "panel_bad";
+    UpdateTestBottonStatus();
+}
+
+function OnDetectFixtureConnection() {
+    document.getElementById("fixture_status").innerHTML = "DETECTING";
+    document.getElementById("test_fixture").className = "panel_bad";
+    UpdateTestBottonStatus();
+}
+
+function BottonRunTestClick() {
+    var testButton = document.getElementById("btn_run_test");
+    testButton.disabled = true;
+    test.sendTestEvent("run_test",
+        {"sn": document.getElementById("serial_number").value});
+    testButton.disabled = false;
+    UpdateTestBottonStatus();
+}
+
+function BottonExitTestClick() {
+    test.sendTestEvent("exit_test", {});
+}
+
+function ResetUiData() {
+    document.getElementById("camera_image").hidden = true;
+    document.getElementById("camera_image").src = "";
+    document.getElementById("analyzed_image").hidden = true;
+    document.getElementById("analyzed_image").src = "";
+    UpdateTestStatus("<span class=\"color_idle\">IDLE</span>");
+    UpdatePrograssBar("0%");
+}
+
+function ClearBuffer() {
+    buf = ""
+}
+
+function AddBuffer(value) {
+    buf += value
+}
+
+function UpdateImage(container_id) {
+    var img = document.getElementById(container_id);
+    img.src = "data:image/jpeg;base64," + buf;
+    img.hidden = false;
+}
+
+function UpdateTestStatus(msg) {
+    var statusText = document.getElementById("test_status");
+    statusText.innerHTML = msg;
+}
+
+function UpdatePrograssBar(progress) {
+    var pBar = document.getElementById("progress_bar");
+    pBar.style.width = progress;
+}