test_server: Add utils to detect continuous 0 from audio data

Add one handy function to detect continous 0 from chameleon. This
function will also save problem samples to .wav file. It can detect for
a very long time by chameleon stream server.
This CL also fixed the lint errors.

Example usage:

  >>> ConnectCrosToLineIn()
  >>> p.StartCapturingAudio(6)

  (do someting on DUT to play
   audio to headphone)

  >>> DetectAudioValue0()

BUG=None
TEST=Test audio_AudioBasicHDMI and audio_AudioBasicHeadphone.
     Test if we can detect the value by HDMI and audiojack interfaces.

Change-Id: I5b20e0cb039c083c823e6f4bb3b6d78e03c88ed5
Reviewed-on: https://chromium-review.googlesource.com/424256
Commit-Ready: Hsu Wei-Cheng <mojahsu@chromium.org>
Tested-by: Hsu Wei-Cheng <mojahsu@chromium.org>
Reviewed-by: Cheng-Yi Chiang <cychiang@chromium.org>
diff --git a/chameleond/drivers/fpga_tio.py b/chameleond/drivers/fpga_tio.py
index 1dc56bf..7d16e33 100644
--- a/chameleond/drivers/fpga_tio.py
+++ b/chameleond/drivers/fpga_tio.py
@@ -935,7 +935,7 @@
     return self._audio_board is not None
 
   @_AudioMethod(input_only=True)
-  def StartCapturingAudio(self, port_id):
+  def StartCapturingAudio(self, port_id, has_file=True):
     """Starts capturing audio.
 
     Refer to the docstring of StartPlayingEcho about the restriction of
@@ -943,10 +943,11 @@
 
     Args:
       port_id: The ID of the audio input port.
+      has_file: True for saving audio data to file. False otherwise.
     """
     self._SelectInput(port_id)
     logging.info('Start capturing audio from port #%d', port_id)
-    self._flows[port_id].StartCapturingAudio()
+    self._flows[port_id].StartCapturingAudio(has_file)
 
   @_AudioMethod(input_only=True)
   def StopCapturingAudio(self, port_id):
@@ -962,6 +963,8 @@
         of utils.audio.AudioDataFormat for detail.
         Currently, the data format supported is
         dict(file_type='raw', sample_format='S32_LE', channel=8, rate=48000)
+      If we assign parameter has_file=False in StartCapturingAudio, we will get
+      both empty string in path and format.
 
     Raises:
       DriverError: Input is selected to port other than port_id.
@@ -974,6 +977,10 @@
           'The input is selected to %r not %r', self._selected_input, port_id)
     path, data_format = self._flows[port_id].StopCapturingAudio()
     logging.info('Stopped capturing audio from port #%d', port_id)
+    # If there is no path, set it to empty string. Because XMLRPC doesn't
+    # support None as return value.
+    if path is None and data_format is None:
+      return '', ''
     return path, data_format
 
   @_AudioMethod(output_only=True)
diff --git a/chameleond/interface.py b/chameleond/interface.py
index 5fbb171..b26b796 100644
--- a/chameleond/interface.py
+++ b/chameleond/interface.py
@@ -489,7 +489,7 @@
     """
     raise NotImplementedError('DetectResolution')
 
-  def StartCapturingAudio(self, port_id):
+  def StartCapturingAudio(self, port_id, has_file=True):
     """Starts capturing audio.
 
     Refer to the docstring of StartPlayingEcho about the restriction of
@@ -497,6 +497,7 @@
 
     Args:
       port_id: The ID of the audio input port.
+      has_file: True for saving audio data to file. False otherwise.
     """
     raise NotImplementedError('StartCapturingAudio')
 
diff --git a/chameleond/utils/audio_utils.py b/chameleond/utils/audio_utils.py
index aadd990..1324662 100644
--- a/chameleond/utils/audio_utils.py
+++ b/chameleond/utils/audio_utils.py
@@ -6,15 +6,12 @@
 import logging
 import os
 import struct
-import tempfile
-import time
 
 import chameleon_common  # pylint: disable=W0611
 from chameleond.utils import fpga
 from chameleond.utils import ids
 from chameleond.utils import mem
 from chameleond.utils import memory_dumper
-from chameleond.utils import system_tools
 
 
 class AudioCaptureManagerError(Exception):
@@ -52,12 +49,14 @@
     """Starts capturing audio.
 
     Args:
-      file_path: The target file for audio capturing.
+      file_path: The target file for audio capturing. None for no target file.
     """
     self._file_path = file_path
     self._adump.StartDumpingToMemory()
-    self._mem_dumper = memory_dumper.MemoryDumper(file_path, self._adump)
-    self._mem_dumper.start()
+    self._mem_dumper = None
+    if file_path:
+      self._mem_dumper = memory_dumper.MemoryDumper(file_path, self._adump)
+      self._mem_dumper.start()
     logging.info('Started capturing audio.')
 
   def StopCapturingAudio(self):
@@ -74,12 +73,13 @@
     if not self.is_capturing:
       raise AudioCaptureManagerError('Stop Capturing audio before Start')
 
-    self._mem_dumper.Stop()
-    self._mem_dumper.join()
-    start_address, page_count = self._adump.StopDumpingToMemory()
+    if self._mem_dumper:
+      self._mem_dumper.Stop()
+      self._mem_dumper.join()
+    _, page_count = self._adump.StopDumpingToMemory()
     logging.info('Stopped capturing audio.')
 
-    if self._mem_dumper.exitcode:
+    if self._mem_dumper and self._mem_dumper.exitcode:
       raise AudioCaptureManagerError(
           'MemoryDumper was terminated unexpectedly.')
 
@@ -89,7 +89,8 @@
 
     # Workaround for issue crbug.com/574683 where the last two pages should
     # be neglected.
-    self._TruncateFile(2)
+    if self._file_path:
+      self._TruncateFile(2)
 
     return self._adump.audio_data_format_as_dict
 
@@ -105,7 +106,7 @@
     file_size = os.path.getsize(self._file_path)
     new_file_size = file_size - self._adump.PAGE_SIZE * pages
     if new_file_size <= 0:
-     raise AudioCaptureManagerError('Not enough audio data was captured.')
+      raise AudioCaptureManagerError('Not enough audio data was captured.')
 
     with open(self._file_path, 'r+') as f:
       f.truncate(new_file_size)
@@ -382,4 +383,3 @@
       True if generator clock is required for source.
     """
     return source != fpga.AudioSource.RX_I2S
-
diff --git a/chameleond/utils/codec_flow.py b/chameleond/utils/codec_flow.py
index bc89d23..774485c 100644
--- a/chameleond/utils/codec_flow.py
+++ b/chameleond/utils/codec_flow.py
@@ -140,14 +140,22 @@
     """
     return self._audio_capture_manager.is_capturing
 
-  def StartCapturingAudio(self):
-    """Starts capturing audio."""
+  def StartCapturingAudio(self, has_file):
+    """Starts capturing audio.
+
+    Args:
+      has_file: True for saving audio data to file. False otherwise.
+    """
     self._audio_codec.SelectInput(self._CODEC_INPUTS[self._port_id])
     self._audio_route_manager.SetupRouteFromInputToDumper(self._port_id)
-    self._recorded_file = tempfile.NamedTemporaryFile(
-        prefix='audio_', suffix='.raw', delete=False)
-    logging.info('Save captured audio to %s', self._recorded_file.name)
-    self._audio_capture_manager.StartCapturingAudio(self._recorded_file.name)
+    self._recorded_file = None
+    file_path = None
+    if has_file:
+      self._recorded_file = tempfile.NamedTemporaryFile(
+          prefix='audio_', suffix='.raw', delete=False)
+      file_path = self._recorded_file.name
+      logging.info('Save captured audio to %s', file_path)
+    self._audio_capture_manager.StartCapturingAudio(file_path)
 
   def StopCapturingAudio(self):
     """Stops capturing audio.
@@ -157,6 +165,8 @@
       path: The path to the captured audio data.
       format: The dict representation of AudioDataFormat. Refer to docstring
         of utils.audio.AudioDataFormat for detail.
+      If we assign parameter has_file=False in StartCapturingAudio, we will get
+      both None in path and format.
 
     Raises:
       AudioCaptureManagerError: If captured time or page exceeds the limit.
@@ -165,7 +175,10 @@
     data_format = self._audio_capture_manager.StopCapturingAudio()
     self._audio_codec.SelectInput(codec.CodecInput.NONE)
     self.ResetRoute()
-    return self._recorded_file.name, data_format
+    if self._recorded_file:
+      return self._recorded_file.name, data_format
+    else:
+      return None, None
 
   def ResetRoute(self):
     """Resets the audio route."""
diff --git a/chameleond/utils/input_flow.py b/chameleond/utils/input_flow.py
index c3d95d5..4d950f7 100644
--- a/chameleond/utils/input_flow.py
+++ b/chameleond/utils/input_flow.py
@@ -418,16 +418,24 @@
     """Is input flow capturing audio?"""
     return self._audio_capture_manager.is_capturing
 
-  def StartCapturingAudio(self):
-    """Starts capturing audio."""
+  def StartCapturingAudio(self, has_file):
+    """Starts capturing audio.
+
+    Args:
+      has_file: True for saving audio data to file. False otherwise.
+    """
     self._audio_route_manager.SetupRouteFromInputToDumper(self._input_id)
-    self._audio_recorded_file = tempfile.NamedTemporaryFile(
-        prefix='audio_', suffix='.raw', delete=False)
-    logging.info('Save captured audio to %s', self._audio_recorded_file.name)
+    self._audio_recorded_file = None
+    file_path = None
+    if has_file:
+      self._audio_recorded_file = tempfile.NamedTemporaryFile(
+          prefix='audio_', suffix='.raw', delete=False)
+      file_path = self._audio_recorded_file.name
+      logging.info('Save captured audio to %s',
+                   self._audio_recorded_file.name)
     # Resets audio logic for issue crbug.com/623466.
     self._ResetAudioLogic()
-    self._audio_capture_manager.StartCapturingAudio(
-        self._audio_recorded_file.name)
+    self._audio_capture_manager.StartCapturingAudio(file_path)
 
   def StopCapturingAudio(self):
     """Stops capturing audio.
@@ -437,6 +445,8 @@
       path: The path to the captured audio data.
       format: The dict representation of AudioDataFormat. Refer to docstring
         of utils.audio.AudioDataFormat for detail.
+      If we assign parameter has_file=False in StartCapturingAudio, we will get
+      both None in path and format.
 
     Raises:
       AudioCaptureManagerError: If captured time or page exceeds the limit.
@@ -444,7 +454,10 @@
     """
     data_format = self._audio_capture_manager.StopCapturingAudio()
     self.ResetRoute()
-    return self._audio_recorded_file.name, data_format
+    if self._audio_recorded_file:
+      return self._audio_recorded_file.name, data_format
+    else:
+      return None, None
 
   def ResetRoute(self):
     """Resets the audio route."""
diff --git a/chameleond/utils/usb_audio_flow.py b/chameleond/utils/usb_audio_flow.py
index 5adb180..166264c 100644
--- a/chameleond/utils/usb_audio_flow.py
+++ b/chameleond/utils/usb_audio_flow.py
@@ -185,8 +185,14 @@
     self._captured_file_type = capture_configs.file_type
     self._usb_ctrl.SetDriverCaptureConfigs(capture_configs)
 
-  def StartCapturingAudio(self):
-    """Starts recording audio data."""
+  def StartCapturingAudio(self, has_file):
+    """Starts capturing audio.
+
+    Args:
+      has_file: It is only can be True for USB audio.
+    """
+    if not has_file:
+      raise USBAudioFlowError('USB Audio only supports save to file')
     self._supported_data_format = self._usb_ctrl.GetSupportedCaptureDataFormat()
     self._supported_data_format.file_type = self._captured_file_type
     params_list = self._GetAlsaUtilCommandArgs(self._supported_data_format)
diff --git a/client/audio/__init__.py b/client/audio/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/audio/__init__.py
diff --git a/client/audio/audio_value_detector.py b/client/audio/audio_value_detector.py
new file mode 100644
index 0000000..184a84b
--- /dev/null
+++ b/client/audio/audio_value_detector.py
@@ -0,0 +1,185 @@
+# Copyright 2016 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.
+
+"""A class used to detect audio streamed values close to 0."""
+
+import logging
+import os
+import struct
+import time
+import wave
+
+from chameleon_stream_server_proxy import (ChameleonStreamServer,
+                                           RealtimeMode)
+
+
+class AudioValueDetector(object):
+  """The class detects if Chameleon captures continuous audio data close to 0.
+
+    Attributes:
+      _host: The host address of chameleon board.
+      _audio_frame_format: Audio sample format from chameleon.
+      _audio_frame_bytes: Total bytes per audio sample.
+      _continuous_0_per_channel: An array to record how many continuous 0s we've
+          gotten per channel
+      _do_detect_0_per_channel: An array to indicate the detect state per
+          channel.  True for detecting continous 0s. False for detecting non 0.
+      _num_print_frames: Used to print the number of values of the first audio
+          frames.
+          Used to fine tune the margin.
+  """
+  def __init__(self, host):
+    """Creates an AudioValueDetector object.
+
+    Args:
+      host: The chameleon board's host address.
+    """
+    self._host = host
+    # Audio sample from chameleon are 32 bits per channel and it has 8 channels.
+    self._audio_frame_format = '<llllllll'
+    self._audio_frame_bytes = struct.calcsize(self._audio_frame_format)
+    self._continuous_0_per_channel = None
+    self._do_detect_0_per_channel = None
+    self._num_print_frames = 0
+
+  def SaveChameleonAudioDataToWav(self, directory, data, file_name):
+    """Save audio data from chameleon to a wave file.
+
+    This function will use default parameters for the chameleon audio data.
+
+    Args:
+      directory: Save file in which directory.
+      data: audio data.
+      file_name: The file name of the wave file.
+    """
+    num_channels = 8
+    sampwidth = 4
+    framerate = 48000
+    comptype = "NONE"
+    compname = "not compressed"
+    file_path = '%s/%s' % (directory, file_name)
+    logging.warn('Save to %s', file_path)
+    wav_file = wave.open(file_path, 'w')
+    wav_file.setparams((num_channels, sampwidth, framerate, len(data), comptype,
+                        compname))
+    wav_file.writeframes(data)
+    wav_file.close()
+
+  def _DetectAudioDataValues(self, channels, continuous_samples, data, margin):
+    """Detect if we get continuous 0s from chameleon audio data.
+
+    Args:
+      channels: Array of audio channels we want to check.
+      continuous_samples: When continuous_samples samples are closed to 0,
+          do detect 0 event.
+      data: A page of audio data from chameleon.
+      margin: Used to decide if the value is closed to 0. Maximum value is 1.
+
+    Returns:
+      Can save file or not. Save file when detecting continuous 0 or detecting
+      non-zero after continuous 0.
+    """
+    should_save_file = False
+
+    # Detect audio data values sample by sample.
+    offset = 0
+    while offset != len(data):
+      audio_sample = struct.unpack_from(self._audio_frame_format, data, offset)
+      offset = offset + self._audio_frame_bytes
+      for index, channel in enumerate(channels):
+        value = float(abs(audio_sample[channel]))/(1 << 31)
+        if self._num_print_frames:
+          logging.info('Value of channel %d is %f', channel, value)
+        # Value is close to 0.
+        if value < margin:
+          self._continuous_0_per_channel[index] += 1
+          # We've detected continuous 0s on this channel before. This sample is
+          # in the same continous 0s state.
+          if not self._do_detect_0_per_channel[index]:
+            continue
+          if self._continuous_0_per_channel[index] >= continuous_samples:
+            logging.warn('Detected continuous %d 0s of channel %d',
+                         self._continuous_0_per_channel[index], channel)
+            self._do_detect_0_per_channel[index] = False
+            should_save_file = True
+        else:
+          # Value is not close to 0.
+          if not self._do_detect_0_per_channel[index]:
+            # This if section means we get non-0 after continuous 0s were
+            # detected.
+            logging.warn('Continuous %d 0s of channel %d',
+                         self._continuous_0_per_channel[index], channel)
+            self._do_detect_0_per_channel[index] = True
+            should_save_file = True
+          # Reset number of continuous 0s when we detect a non-0 value.
+          self._continuous_0_per_channel[index] = 0
+      if self._num_print_frames:
+        self._num_print_frames -= 1
+    return should_save_file
+
+  def Detect(self, channels, margin, continuous_samples, duration, dump_frames):
+    """Detects if Chameleon captures continuous audio data close to 0.
+
+    This function will get the audio streaming data from stream server and will
+    check if the audio data is close to 0 by the margin parameter.
+    -margin < value < margin will be considered to be close to 0.
+    If there are continuous audio samples close to 0 in the streamed data,
+    test_server will log it and save the audio data to a wav file.
+
+    Args:
+      channels: Array of audio channels we want to check.
+          E.g. [0, 1] means we only care about channel 0 and channel 1.
+      margin: Used to decide if the value is closed to 0. Maximum value is 1.
+      continuous_samples: When continuous_samples samples are closed to 0,
+          trigger event.
+      duration: The duration of monitoring in seconds.
+      dump_frames: When event happens, how many audio frames we want to save to
+          file.
+    """
+    # Create a new directory for storing audio files.
+    directory = 'detect0_%s' % time.strftime('%Y%m%d%H%M%S', time.localtime())
+    if not os.path.exists(directory):
+      os.mkdir(directory)
+
+    dump_bytes = self._audio_frame_bytes * dump_frames
+
+    self._num_print_frames = 10
+    self._continuous_0_per_channel = [0] * len(channels)
+    self._do_detect_0_per_channel = [True] * len(channels)
+    audio_data = ''
+
+    stream = ChameleonStreamServer(self._host)
+    stream.connect()
+    stream.reset_audio_session()
+    stream.dump_realtime_audio_page(RealtimeMode.BestEffort)
+    start_time = time.time()
+    logging.info('Start to detect continuous 0s.')
+    logging.info('Channels=%r, margin=%f, continuous_samples=%d, '
+                 'duration=%d seconds, dump_frames=%d.', channels, margin,
+                 continuous_samples, duration, dump_frames)
+    while True:
+      audio_page = stream.receive_realtime_audio_page()
+      if not audio_page:
+        logging.warn('No audio page, there may be a socket errror.')
+        break
+      # We've checked None before, so we can disable the false alarm.
+      (page_count, data) = audio_page  # pylint: disable=unpacking-non-sequence
+      audio_data += data
+
+      # Only keep needed volume of data for saving memory usage.
+      audio_data = audio_data[-dump_bytes:]
+
+      should_save_file = self._DetectAudioDataValues(channels,
+                                                     continuous_samples, data,
+                                                     margin)
+      if should_save_file:
+        file_name = 'audio_%d.wav' % page_count
+        self.SaveChameleonAudioDataToWav(directory, audio_data, file_name)
+        audio_data = ''
+
+      current_time = time.time()
+      if current_time - start_time > duration:
+        stream.stop_dump_realtime_audio_page()
+        logging.warn('Timeout stop detect.')
+        break
diff --git a/client/chameleon_stream_server_proxy.py b/client/chameleon_stream_server_proxy.py
new file mode 100644
index 0000000..82a1c21
--- /dev/null
+++ b/client/chameleon_stream_server_proxy.py
@@ -0,0 +1,553 @@
+# Copyright 2016 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.
+
+"""This module provides the utilities for chameleon streaming server usage.
+
+Sample Code for dumping real-time video frames:
+  stream = ChameleonStreamServer(IP)
+  stream.reset_video_session()
+
+  chameleon_proxy.StartCapturingVideo(port)
+  stream.dump_realtime_video_frame(False, RealtimeMode.BestEffort)
+  while True:
+    video_frame = stream.receive_realtime_video_frame()
+    if not video_frame:
+      break
+    (frame_number, width, height, channel, data) = video_frame
+    image = Image.fromstring('RGB', (width, height), data)
+    image.save('%d.bmp' % frame_number)
+
+Sample Code for dumping real-time audio pages:
+  stream = ChameleonStreamServer(IP)
+  stream.reset_audio_session()
+
+  chameleon_proxy.StartCapturingAudio(port)
+  stream.dump_realtime_audio_page(RealtimeMode.BestEffort)
+  f = open('audio.raw',  'w')
+  while True:
+    audio_page = stream.receive_realtime_audio_page()
+    if not audio_page:
+      break
+    (page_count, data) = audio_page
+    f.write(data)
+"""
+
+import collections
+import logging
+import socket
+from struct import calcsize, pack, unpack
+
+
+CHAMELEON_STREAM_SERVER_PORT = 9994
+SUPPORT_MAJOR_VERSION = 1
+SUPPORT_MINOR_VERSION = 0
+
+
+class StreamServerVersionError(Exception):
+  """Version is not compatible between client and server."""
+  pass
+
+
+class ErrorCode(object):
+  """Error codes of response from the stream server."""
+  OK = 0
+  NON_SUPPORT_COMMAND = 1
+  ARGUMENT = 2
+  REAL_TIME_STREAM_EXISTS = 3
+  VIDEO_MEMORY_OVERFLOW_STOP = 4
+  VIDEO_MEMORY_OVERFLOW_DROP = 5
+  AUDIO_MEMORY_OVERFLOW_STOP = 6
+  AUDIO_MEMORY_OVERFLOW_DROP = 7
+  MEMORY_ALLOC_FAIL = 8
+
+
+class RealtimeMode(object):
+  """Realtime mode of dumping data."""
+  # Stop dump when memory overflow
+  StopWhenOverflow = 1
+
+  # Drop data when memory overflow
+  BestEffort = 2
+
+  # Strings used for logging.
+  LogStrings = ['None', 'Stop when overflow', 'Best effort']
+
+
+class ChameleonStreamServer(object):
+  """This class provides easy-to-use APIs to access the stream server."""
+
+  # Main message types.
+  _REQUEST_TYPE = 0
+  _RESPONSE_TYPE = 1
+  _DATA_TYPE = 2
+
+  # uint16 type, uint16 error_code, uint32 length.
+  packet_head_struct = '!HHL'
+
+  # Message types.
+  Message = collections.namedtuple('Message', ['type',
+                                               'request_struct',
+                                               'response_struct',
+                                               'data_struct'])
+  _RESET_MSG = Message(0, None, None, None)
+  # Response: uint8 major, uint8 minor.
+  _GET_VERSION_MSG = Message(1, None, '!BB', None)
+  # Request: unt16 screen_width, uint16 screen_height.
+  _CONFIG_VIDEO_STREAM_MSG = Message(2, '!HH', None, None)
+  # Request: uint8 shrink_width, uint8 shrink_height.
+  _CONFIG_SHRINK_VIDEO_STREAM_MSG = Message(3, '!BB', None, None)
+  # Request: uint32 memory_address1, uint32 memory_address2,
+  #          uint16 number_of_frames.
+  # Data: uint32 frame_number, uint16 width, uint16 height, uint8 channel,
+  #       uint8 padding[3]
+  _DUMP_VIDEO_FRAME_MSG = Message(4, '!LLH', None, '!LHHBBBB')
+  # Request: uint8 is_dual, uint8 mode.
+  # Data: uint32 frame_number, uint16 width, uint16 height, uint8 channel,
+  #       uint8 padding[3]
+  _DUMP_REAL_TIME_VIDEO_FRAME_MSG = Message(5, '!BB', None, '!LHHBBBB')
+  _STOP_DUMP_VIDEO_FRAME_MSG = Message(6, None, None, None)
+  # Request: uint8 mode.
+  # Data: uint32 page_count.
+  _DUMP_REAL_TIME_AUDIO_PAGE = Message(7, '!B', None, '!L')
+  _STOP_DUMP_AUDIO_PAGE = Message(8, None, None, None)
+
+  _PACKET_HEAD_SIZE = 8
+
+  def __init__(self, hostname, port=CHAMELEON_STREAM_SERVER_PORT):
+    """Constructs a ChameleonStreamServer.
+
+    Args:
+      hostname: Hostname of stream server.
+      port: Port number the stream server is listening on.
+    """
+    self._video_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    self._audio_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    self._hostname = hostname
+    self._port = port
+    # Used for non-realtime dump video frames.
+    self._remain_frame_count = 0
+    self._is_realtime_video = False
+    self._is_realtime_audio = False
+
+  def _get_request_type(self, message):
+    """Get the request type of the message.
+
+    Args:
+      message: Message namedtuple.
+
+    Returns:
+      Request message type.
+    """
+    return (self._REQUEST_TYPE << 8) | message.type
+
+  def _get_response_type(self, message):
+    """Get the response type of the message.
+
+    Args:
+      message: Message namedtuple.
+
+    Returns:
+      Response message type.
+    """
+    return (self._RESPONSE_TYPE << 8) | message.type
+
+  def _is_data_type(self, message_type):
+    """Check if the message type is data type.
+
+    Args:
+      message_type: Message type
+
+    Returns:
+      Non 0 if the message is data type. otherwise 0.
+    """
+    return (self._DATA_TYPE << 8) & message_type
+
+  def _receive_whole_packet(self, sock):
+    """Receive one whole packet, contains packet head and content.
+
+    Args:
+      sock: Which socket to be used.
+
+    Returns:
+      A tuple with 4 elements: message_type, error code, length and content.
+
+    Raises:
+      ValueError if we can't receive data from server.
+    """
+    # receive packet header
+    data = sock.recv(self._PACKET_HEAD_SIZE)
+    if not data:
+      raise ValueError('Receive no data from server')
+
+    while len(data) != self._PACKET_HEAD_SIZE:
+      remain_length = self._PACKET_HEAD_SIZE - len(data)
+      recv_content = sock.recv(remain_length)
+      data += recv_content
+
+    message_type, error_code, length = unpack(self.packet_head_struct, data)
+
+    # receive content
+    content = ''
+    remain_length = length
+    while remain_length:
+      recv_content = sock.recv(remain_length)
+      if not recv_content:
+        raise ValueError('Receive no data from server')
+      remain_length -= len(recv_content)
+      content += recv_content
+
+    if error_code != ErrorCode.OK:
+      logging.warn('Receive error code %d, %r', error_code, content)
+
+    return (message_type, error_code, length, content)
+
+  def _send_and_receive(self, packet, sock, check_error=True):
+    """Send packet to server and receive response from server.
+
+    Args:
+      packet: The packet to be sent.
+      sock: Which socket to be used.
+      check_error: Check the error code. If this is True, this function will
+          check the error code from response and raise exception if the error
+          code is not OK.
+
+    Returns:
+      The response packet from server. A tuple with 4 elements contains
+      message_type, error code, length and content.
+
+    Raises:
+      ValueError if check_error and error code is not OK.
+    """
+    sock.send(packet)
+    packet = self._receive_whole_packet(sock)
+    if packet and check_error:
+      (_, error_code, _, _) = packet
+      if error_code != ErrorCode.OK:
+        raise ValueError('Error code is not OK')
+
+    return packet
+
+  def _generate_request_packet(self, message, *args):
+    """Generate whole request packet with parameters.
+
+    Args:
+      message: Message namedtuple.
+      *args: Packet contents.
+
+    Returns:
+      The whole request packet content.
+    """
+    if message.request_struct:
+      content = pack(message.request_struct, *args)
+    else:
+      content = ''
+
+    # Create header.
+    head = pack(self.packet_head_struct,
+                self._get_request_type(message),
+                ErrorCode.OK, len(content))
+
+    return head + content
+
+  def _receive_video_frame(self):
+    """Receive one video frame from server.
+
+    This function assumes it only can receive video frame data packet
+    from server. Error code will indicate success or not.
+
+    Returns:
+      If error code is OK, a decoded values will be stored in a tuple
+      (error_code, frame number, width, height, channel, data).
+      If error code is not OK, it will return a tuple (error code, content). The
+      content is the error message from server.
+
+    Raises:
+      ValueError if packet is not data packet.
+    """
+    (message, error_code, _, content) = self._receive_whole_packet(
+        self._video_sock)
+    if error_code != ErrorCode.OK:
+      return (error_code, content)
+
+    if not self._is_data_type(message):
+      raise ValueError('Message is not data')
+
+    video_frame_head_size = calcsize(self._DUMP_VIDEO_FRAME_MSG.data_struct)
+    frame_number, width, height, channel, _, _, _ = unpack(
+        self._DUMP_VIDEO_FRAME_MSG.data_struct, content[:video_frame_head_size])
+    data = content[video_frame_head_size:]
+    return (error_code, frame_number, width, height, channel, data)
+
+  def _get_version(self):
+    """Get the version of the server.
+
+    Returns:
+      A tuple with Major and Minor number of the server.
+    """
+    packet = self._generate_request_packet(self._GET_VERSION_MSG)
+    (_, _, _, content) = self._send_and_receive(packet, self._video_sock)
+    return unpack(self._GET_VERSION_MSG.response_struct, content)
+
+  def _check_version(self):
+    """Check if this client is compatible with the server.
+
+    The major number must be the same and the minor number of the server
+    must larger then the client's.
+
+    Returns:
+      Compatible or not
+    """
+    (major, minor) = self._get_version()
+    logging.debug('Major %d, minor %d', major, minor)
+    return major == SUPPORT_MAJOR_VERSION and minor >= SUPPORT_MINOR_VERSION
+
+  def connect(self):
+    """Connect to the server and check the compatibility.
+
+    Raises:
+      StreamServerVersionError if client is not compitable with server.
+    """
+    server_address = (self._hostname, self._port)
+    logging.info('connecting to %s:%s', self._hostname, self._port)
+    self._video_sock.connect(server_address)
+    self._audio_sock.connect(server_address)
+    if not self._check_version():
+      raise StreamServerVersionError()
+
+  def reset_video_session(self):
+    """Reset the video session."""
+    logging.info('Reset session')
+    packet = self._generate_request_packet(self._RESET_MSG)
+    self._send_and_receive(packet, self._video_sock)
+
+  def reset_audio_session(self):
+    """Reset the audio session.
+
+    For audio, we don't need to reset any thing.
+    """
+    pass
+
+  def config_video_stream(self, width, height):
+    """Configure the properties of the non-realtime video stream.
+
+    Args:
+      width: The screen width of the video frame by pixel per channel.
+      height: The screen height of the video frame by pixel per channel.
+    """
+    logging.info('Config video, width %d, height %d', width, height)
+    packet = self._generate_request_packet(self._CONFIG_VIDEO_STREAM_MSG, width,
+                                           height)
+    self._send_and_receive(packet, self._video_sock)
+
+  def config_shrink_video_stream(self, shrink_width, shrink_height):
+    """Configure the shrink operation of the video frame dump.
+
+    Args:
+      shrink_width: Shrink (shrink_width+1) pixels to 1 pixel when do video
+          dump. 0 means no shrink.
+      shrink_height: Shrink (shrink_height+1) to 1 height when do video dump.
+          0 means no shrink.
+    """
+    logging.info('Config shrink video, shirnk_width %d, shrink_height %d',
+                 shrink_width, shrink_height)
+    packet = self._generate_request_packet(self._CONFIG_VIDEO_STREAM_MSG,
+                                           shrink_width, shrink_height)
+    self._send_and_receive(packet, self._video_sock)
+
+  def dump_video_frame(self, count, address1, address2):
+    """Ask server to dump video frames.
+
+    User must use receive_video_frame() to receive video frames after
+    calling this API.
+
+    Sample Code:
+      address = chameleon_proxy.GetCapturedFrameAddresses(0)
+      count = chameleon_proxy.GetCapturedFrameCount()
+      server.dump_video_frame(count, int(address), 0)
+      while True:
+        video_frame = server.receive_video_frame()
+        if not video_frame:
+          break
+        (frame_number, width, height, channel, data) = video_frame
+        image = Image.fromstring('RGB', (width, height), data)
+        image.save('%s.bmp' % frame_number)
+
+    Args:
+      count: Specify number of video frames.
+      address1: Dump memory address1.
+      address2: Dump memory address2. If it is 0. It means we only dump from
+          address1.
+    """
+    logging.info('dump video frame count %d, address1 0x%x, address2 0x%x',
+                 count, address1, address2)
+    packet = self._generate_request_packet(self._DUMP_VIDEO_FRAME_MSG, address1,
+                                           address2, count)
+    self._send_and_receive(packet, self._video_sock)
+    self._remain_frame_count = count
+
+  def dump_realtime_video_frame(self, is_dual, mode):
+    """Ask server to dump realtime video frames.
+
+    User must use receive_realtime_video_frame() to receive video frames
+    after calling this API.
+
+    Sample Code:
+      server.dump_realtime_video_frame(False,
+                       RealtimeMode.StopWhenOverflow)
+      while True:
+        video_frame = server.receive_realtime_video_frame()
+        if not video_frame:
+          break
+        (frame_number, width, height, channel, data) = video_frame
+        image = Image.fromstring('RGB', (width, height), data)
+        image.save('%s.bmp' % frame_number)
+
+    Args:
+      is_dual: False: means only dump from channel1, True: means dump from dual
+          channels.
+      mode: The values of RealtimeMode.
+    """
+    logging.info('dump realtime video frame is_dual %d, mode %s', is_dual,
+                 RealtimeMode.LogStrings[mode])
+    packet = self._generate_request_packet(self._DUMP_REAL_TIME_VIDEO_FRAME_MSG,
+                                           is_dual, mode)
+    self._send_and_receive(packet, self._video_sock)
+    self._is_realtime_video = True
+
+  def receive_video_frame(self):
+    """Receive one video frame from server after calling dump_video_frame().
+
+    This function assumes it only can receive video frame data packet
+    from server. Error code will indicate success or not.
+
+    Returns:
+      A tuple with video frame information.
+      (frame number, width, height, channel, data), None if error happens.
+    """
+    if not self._remain_frame_count:
+      return None
+    self._remain_frame_count -= 1
+    frame_info = self._receive_video_frame()
+    if frame_info[0] != ErrorCode.OK:
+      self._remain_frame_count = 0
+      return None
+    return frame_info[1:]
+
+  def receive_realtime_video_frame(self):
+    """Receive one video frame from server.
+
+    After calling dump_realtime_video_frame(). The video frame may be dropped if
+    we use BestEffort mode. We can detect it by the frame number.
+
+    This function assumes it only can receive video frame data packet
+    from server. Error code will indicate success or not.
+
+    Returns:
+      A tuple with video frame information.
+      (frame number, width, height, channel, data), None if error happens or
+      no more frames.
+    """
+    if not self._is_realtime_video:
+      return None
+
+    frame_info = self._receive_video_frame()
+    # We can still receive video frame for drop case.
+    while frame_info[0] == ErrorCode.VIDEO_MEMORY_OVERFLOW_DROP:
+      frame_info = self._receive_video_frame()
+
+    if frame_info[0] != ErrorCode.OK:
+      return None
+
+    return frame_info[1:]
+
+  def stop_dump_realtime_video_frame(self):
+    """Ask server to stop dump realtime video frame."""
+    if not self._is_realtime_video:
+      return
+    packet = self._generate_request_packet(self._STOP_DUMP_VIDEO_FRAME_MSG)
+    self._video_sock.send(packet)
+    # Drop video frames until receive _StopDumpVideoFrame response.
+    while True:
+      (message, _, _, _) = self._receive_whole_packet(self._video_sock)
+      if message == self._get_response_type(self._STOP_DUMP_VIDEO_FRAME_MSG):
+        break
+    self._is_realtime_video = False
+
+  def dump_realtime_audio_page(self, mode):
+    """Ask server to dump realtime audio pages.
+
+    User must use receive_realtime_audio_page() to receive audio pages
+    after calling this API.
+
+    Sample Code for BestEffort:
+      server.dump_realtime_audio_page(RealtimeMode.kBestEffort)
+      f = open('audio.raw'), 'w')
+      while True:
+        audio_page = server.receive_realtime_audio_page()
+        if audio_page:
+          break
+        (page_count, data) = audio_page
+        f.write(data)
+
+    Args:
+      mode: The values of RealtimeMode.
+
+    Raises:
+      ValueError if error code from response is not OK.
+    """
+    logging.info('dump realtime audio page mode %s',
+                 RealtimeMode.LogStrings[mode])
+    packet = self._generate_request_packet(self._DUMP_REAL_TIME_AUDIO_PAGE,
+                                           mode)
+    self._send_and_receive(packet, self._audio_sock)
+    self._is_realtime_audio = True
+
+  def receive_realtime_audio_page(self):
+    """Receive one audio page from server.
+
+    After calling dump_realtime_audio_page(). The behavior is the same as
+    receive_realtime_video_frame(). The audio page may be dropped if we use
+    BestEffort mode. We can detect it by the page count.
+
+    This function assumes it can receive audio page data packet
+    from server. Error code will indicate success or not.
+
+    Returns:
+      A tuple with audio page information: (page count, data) None if error
+      happens or no more frames.
+
+    Rraises:
+      ValueError if packet is not data packet.
+    """
+    if not self._is_realtime_audio:
+      return None
+    (message, error_code, _, content) = self._receive_whole_packet(
+        self._audio_sock)
+    # We can still receive audio page for drop case.
+    while error_code == ErrorCode.AUDIO_MEMORY_OVERFLOW_DROP:
+      (message, error_code, _, content) = self._receive_whole_packet(
+          self._audio_sock)
+
+    if error_code != ErrorCode.OK:
+      return None
+    if not self._is_data_type(message):
+      raise ValueError('Message is not data')
+
+    page_count = unpack(self._DUMP_REAL_TIME_AUDIO_PAGE.data_struct,
+                        content[:4])[0]
+    data = content[4:]
+    return (page_count, data)
+
+  def stop_dump_realtime_audio_page(self):
+    """Ask server to stop dump realtime audio page."""
+    if not self._is_realtime_audio:
+      return
+    packet = self._generate_request_packet(self._STOP_DUMP_AUDIO_PAGE)
+    self._audio_sock.send(packet)
+    # Drop audio pages until receive _StopDumpAudioPage response.
+    while True:
+      (message, _, _, _) = self._receive_whole_packet(self._audio_sock)
+      if message == self._get_response_type(self._STOP_DUMP_AUDIO_PAGE):
+        break
+    self._is_realtime_audio = False
diff --git a/utils/test_server b/client/test_server.py
similarity index 62%
rename from utils/test_server
rename to client/test_server.py
index c775112..2fadacc 100755
--- a/utils/test_server
+++ b/client/test_server.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python2
 # Copyright 2015 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.
@@ -14,6 +14,8 @@
 import subprocess
 import xmlrpclib
 
+from audio.audio_value_detector import AudioValueDetector
+
 
 def ShowMessages(proxy):
   """Shows the messages for usage.
@@ -40,20 +42,50 @@
       p.StartCapturingAudio(%d) to capture from LineIn.
       p.StopCapturingAudio(%d) to stop capturing from LineIn.
       p.Plug(%d) to plug HDMI.
-      p.UnPlug(%d) to unplug HDMI.''',
-      '\n      '.join(port_messages), linein_port, linein_port,
-      hdmi_port, hdmi_port)
+      p.Unplug(%d) to unplug HDMI.''', '\n      '.join(port_messages),
+               linein_port, linein_port, hdmi_port, hdmi_port)
 
 
-def StartInteractiveShell(p, options):
+def DetectAudioValue0(channels=None, margin=0.01, continuous_samples=5,
+                      duration=3600, dump_samples=48000):
+  """Detects if Chameleon captures continuous audio data close to 0.
+
+  This function will get the audio streaming data from stream server and will
+  check if the audio data is close to 0 by the margin parameter.
+  -margin < value < margin will be considered to be close to 0.
+  If there are continuous audio samples close to 0 in the streamed data,
+  test_server will log it and save the audio data to a wav file.
+
+  E.g.
+  >>> ConnectCrosToLineIn()
+  >>> p.StartCapturingAudio(6, False)
+  >>> DetectAudioValue0(duration=24*3600, margin=0.001)
+
+  Args:
+    channels: Array of audio channels we want to check.
+        E.g. [0, 1] means we only care about channel 0 and channel 1.
+    margin: Used to decide if the value is closed to 0. Maximum value is 1.
+    continuous_samples: When continuous_samples samples are closed to 0, trigger
+        event.
+    duration: The duration of monitoring in seconds.
+    dump_samples: When event happens, how many audio samples we want to
+        save to file.
+  """
+  if not channels:
+    channels = [0, 1]
+  detecter = AudioValueDetector(options.host)  # pylint: disable=undefined-variable
+  detecter.Detect(channels, margin, continuous_samples, duration, dump_samples)
+  return True
+
+
+def StartInteractiveShell(p, options):  # pylint: disable=unused-argument
   """Starts an interactive shell.
 
   Args:
     p: The xmlrpclib.ServerProxy to chameleond.
     options: The namespace from argparse.
-
   """
-  vars = globals()
+  vars = globals()  # pylint: disable=redefined-builtin
   vars.update(locals())
   readline.set_completer(rlcompleter.Completer(vars).complete)
   readline.parse_and_bind("tab: complete")
@@ -64,8 +96,8 @@
 def ParseArgs():
   """Parses the arguments.
 
-  Returns: the namespace containing parsed arguments.
-
+  Returns:
+    the namespace containing parsed arguments.
   """
   parser = argparse.ArgumentParser(
       description='Connect to Chameleond and use interactive shell.',
@@ -94,12 +126,11 @@
 
   Args:
     remote_path: The file to copy from Chameleon host.
-
   """
   basename = os.path.basename(remote_path)
   # options is already in the namespace.
   subprocess.check_call(
-      ['scp', 'root@%s:%s' % (options.host, remote_path), basename])
+      ['scp', 'root@%s:%s' % (options.host, remote_path), basename])  # pylint: disable=undefined-variable
   subprocess.check_call(
       ['sox', '-b', '32', '-r', '48000', '-c', '8', '-e', 'signed',
        basename, '-c', '2', basename + '.wav'])
@@ -107,8 +138,8 @@
 
 def ConnectCrosToLineIn():
   """Connects a audio bus path from Cros headphone to Chameleon LineIn."""
-  p.AudioBoardConnect(1, 'Cros device headphone')
-  p.AudioBoardConnect(1, 'Chameleon FPGA line-in')
+  p.AudioBoardConnect(1, 'Cros device headphone') # pylint: disable=undefined-variable
+  p.AudioBoardConnect(1, 'Chameleon FPGA line-in') # pylint: disable=undefined-variable
 
 
 def Main():