Add heatmapplot to touch_firmware_test

This patch add heatmapplot to touch_firmware_test. Heatmapplot is a tool
to plot heatmap. Currently the tool only support plotting bar chart of
profile sensor. The plotting script call heatmap_tool to dump the
heatmap from device. Heatmap_tool is a user space, board specific
device.

TEST=run heatmapplot locally
BUG=b:62343950

Change-Id: If8d5217e05b5dc8d79c149107b67f2f7e6f40cb8
Reviewed-on: https://chromium-review.googlesource.com/567687
Commit-Ready: Jingkui Wang <jkwang@google.com>
Tested-by: Jingkui Wang <jkwang@google.com>
Reviewed-by: Charlie Mooney <charliemooney@chromium.org>
diff --git a/heatmap/__init__.py b/heatmap/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/heatmap/__init__.py
diff --git a/heatmap/chromeos_heatmapplot_wrapper.sh b/heatmap/chromeos_heatmapplot_wrapper.sh
new file mode 100755
index 0000000..ba3c90b
--- /dev/null
+++ b/heatmap/chromeos_heatmapplot_wrapper.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+# Copyright 2017 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 script is deployed directly onto Chromebooks, and is only intended
+# to be run from a ChromeOS device. It is a wrapper for heatmapplot.py that
+# makes it simpler to run heatmapplot on the DUT itself.  If you are attempting
+# to plot heatmap on a remote DUT (such as one connected to your computer via
+# ssh) this script is NOT what you want, you should look into running
+# heatmapplot.py directly.
+
+PROG="heatmapplot.py"
+
+# A local die function to print the message and then exit
+die() {
+  echo -e "$@"
+  exit 1
+}
+
+FLAGS_HELP="USAGE: $PROG [flags]"
+
+# Search the heatmapplot directory.
+# Stop at the first found heatmapplot directory. Priority is given to /usr/lib*.
+DIRS="/usr/lib* /usr/local/lib*"
+for d in $DIRS; do
+  PROG_FILE="$(find $d -name $PROG -type f -print -quit)"
+  if [ -n "$PROG_FILE" ]; then
+    break
+  fi
+done
+
+if [ -z "$PROG_FILE" ]; then
+  die "Fail to find the path of $PROG."
+fi
+
+# Must run heatmapplot as root as it needs to access system device nodes.
+if [ $USER != root ]; then
+  die "Please run $PROG as root."
+fi
+
+# Tell the user to type URL in chrome as there is no reliable way to
+# launch a chrome tab from command line in chrome os.
+echo "Please type \"localhost\" in the browser."
+echo "Please Press ctrl-c to terminate the heatmapplot server."
+
+echo "Start $PROG server..."
+[ "$FLAGS_grab" = "$FLAGS_FALSE" ] && grab_option="--nograb"
+exec python "${PROG_FILE}" --behind_firewall
diff --git a/heatmap/heatmap.html b/heatmap/heatmap.html
new file mode 100644
index 0000000..de829ae
--- /dev/null
+++ b/heatmap/heatmap.html
@@ -0,0 +1,36 @@
+<!--
+Copyright 2017 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.
+-->
+
+<html>
+<head>
+  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
+  <script src="heatmap.js"></script>
+</head>
+
+<body onload="InitChart()">
+  <div>
+    <select onChange="setPole(this.selectedIndex)">
+      <option value=0>Normal</option>
+      <option value=1>Lower</option>
+      <option value=2>Higher</option>
+    </select>
+    <input onclick="
+           if (this.value === 'Start Record') {
+              this.value = 'Stop and Save';
+              window.heatMapChart.eventBuffer.startRecord();
+           } else {
+              this.value = 'Start Record';
+              window.heatMapChart.eventBuffer.stopRecordAndSave();
+           }
+    " type="button" value="Start Record">
+    </input>
+  </div>
+  <div id="xChart"> </div>
+  <div id="yChart"> </div>
+  <div id="websocketUrl" hidden>%(websocketUrl)s</div>
+
+</body>
+</html>
diff --git a/heatmap/heatmap.js b/heatmap/heatmap.js
new file mode 100644
index 0000000..ff5d602
--- /dev/null
+++ b/heatmap/heatmap.js
@@ -0,0 +1,265 @@
+// Copyright 2017 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.
+
+
+/**
+ * HeatMapEventBuffer buffer the heatmap event.
+ * @constructor
+ * @param {int} maxSize max size of the buffer.
+ */
+function HeatMapEventBuffer(maxSize = 50) {
+  this.maxSize = maxSize;
+  this.rawData = [];
+  this.avgData = [];
+  this.floorLevel = 0;
+  this.sigData = [];
+  this.sigData3 = [];
+}
+
+/**
+ * Following are set of helper functions used to calculate avg, sig, and floor.
+ */
+
+// Elementwise sum of 2d array.
+ElementwiseSum = arr=>arr.reduce((arr1,arr2)=>arr1.map((a,i)=>a+arr2[i]));
+// Average of 2d array.
+Average2D = arr=>ElementwiseSum(arr).map(a=>a/arr.length);
+// Max of 1d array.
+Max1D = arr=>arr.reduce((a,b)=>Math.max(a,b));
+// Average of 1d Array.
+Average1D = arr=>arr.reduce((a,b)=>a+b)/arr.length
+
+/**
+ * Function to add new event to the buffer.
+ * @param {object} newEventData new heatmap data
+ */
+HeatMapEventBuffer.prototype.appendEvent = function(newEventData) {
+  if (this.rawData.length >= this.maxSize) {
+    this.rawData.shift();
+  }
+  if (this.avgData.length >= this.maxSize) {
+    this.avgData.shift();
+  }
+  if (this.sigData.length >= this.maxSize) {
+    this.sigData.shift();
+  }
+  this.rawData[this.rawData.length] = newEventData;
+  this.avgData = Average2D(this.rawData);
+  variance = Average2D(this.rawData.map(arr=>arr.map(
+      (a, i)=>Math.pow(a - this.avgData[i], 2))));
+  this.sigData = variance.map(x=>Math.sqrt(x));
+  this.sigData3 = this.sigData.map((a,i)=>3*a + this.avgData[i]);
+  this.floorLevel = Average1D(this.rawData.map(arr=>Max1D(arr)));
+}
+
+/**
+ * Function to get dataTable from select columns. The dataTable will be used to
+ * draw the linechart.
+ * @return {google.visualization.DataTable}
+ */
+HeatMapEventBuffer.prototype.getDataTable = function() {
+  var data = new google.visualization.DataTable();
+  data.addColumn('number', 'value');
+  data.addColumn('number', 'Floor');
+  data.addColumn('number', 'Sigma');
+  data.addColumn('number', 'Raw');
+  data.addColumn('number', 'Avg');
+
+  rows = [];
+  lastIndex = this.rawData.length - 1;
+  if (!this.rawData.length) {
+    return data;
+  }
+  for (var i = 0; i < this.rawData[lastIndex].length; i++) {
+    rows[rows.length] = [i, this.floorLevel, this.sigData3[i],
+      this.rawData[lastIndex][i], this.avgData[i]]
+  }
+
+  data.addRows(rows);
+  return data;
+}
+
+/**
+ * EventBufferWrapper wrapper for x and y buffer.
+ * @constructor
+ */
+function EventBufferWrapper() {
+  this.xBuffer = new HeatMapEventBuffer();
+  this.yBuffer = new HeatMapEventBuffer();
+  this.startRecordFlag = false;
+  this.serilizedHeatMap = "";
+}
+
+/**
+ * Function to add new event to the correct buffer.
+ * @param {object} newEvent new heatmap event
+ */
+EventBufferWrapper.prototype.appendEvent = function(newEvent) {
+  if (newEvent.sensor == 'x') {
+    this.xBuffer.appendEvent(newEvent.data);
+  } else if (newEvent.sensor == 'y') {
+    this.yBuffer.appendEvent(newEvent.data);
+  }
+  if (this.startRecordFlag) {
+    this.serilizedHeatMap += newEvent.sensor;
+    this.serilizedHeatMap += ",";
+    for (var i = 0; i < newEvent.data.length; i++) {
+      this.serilizedHeatMap += newEvent.data[i];
+      this.serilizedHeatMap += ",";
+    }
+    this.serilizedHeatMap += "\n";
+  }
+}
+
+/**
+ * Function to get dataTable for the selected sensor.
+ * @param {string} sensor x or y.
+ */
+EventBufferWrapper.prototype.getDataTable = function(sensor) {
+  if (sensor == 'x') {
+    return this.xBuffer.getDataTable();
+  } else if (sensor == 'y') {
+    return this.yBuffer.getDataTable();
+  }
+}
+
+/**
+ * Function to start recording heatmap.
+ * @param {string} sensor x or y.
+ */
+EventBufferWrapper.prototype.startRecord = function() {
+  this.startRecordFlag = true;
+  this.serilizedHeatMap = "flag,";
+  for (var i = 0; i < 64; i ++) {
+    this.serilizedHeatMap += i;
+    this.serilizedHeatMap += ",";
+  }
+  this.serilizedHeatMap += "\n";
+}
+
+/**
+ * Function to stop recording heatmap and save the log file.
+ * @param {string} sensor x or y.
+ */
+EventBufferWrapper.prototype.stopRecordAndSave = function() {
+  this.startRecordFlag = false;
+  var element = document.createElement('a');
+  element.setAttribute('href',
+      'data:text/plain;charset=utf-8,'
+      + encodeURIComponent(this.serilizedHeatMap));
+  element.setAttribute('download', "heatmap.csv");
+
+  element.style.display = 'none';
+  document.body.appendChild(element);
+
+  element.click();
+  document.body.removeChild(element);
+}
+
+/**
+ * Draw chart for HeatMap readings.
+ * @constructor
+ * @param {Element} xChartDiv the div to draw x chart.
+ * @param {Element} yChartDiv the div to draw y chart.
+ */
+function HeatMapChart(xChartDiv, yChartDiv) {
+  this.eventBuffer = new EventBufferWrapper();
+
+  this.initCharts(xChartDiv, yChartDiv);
+  this.drawCharts();
+}
+
+/**
+ * Get chart options.
+ * @param {string} vName Name of the v axis.
+ */
+HeatMapChart.prototype.getChartOptions = function(vName) {
+  var options = {
+    vAxis: {
+      title: vName,
+      minValue: 1000,
+      maxValue: 66000,
+    },
+    connectSteps: false,
+    // The order of series is: Floor, Sigma, Raw, Average
+    colors:['pink','yellow', '#000066', '#99ff33'],
+    series: {
+      0: {areaOpacity: 0.3},
+      1: {areaOpacity: 1},
+      2: {areaOpacity: 1},
+      3: {areaOpacity: 0}},
+  };
+  return options;
+}
+
+/**
+ * This function init all the line charts.
+ * @param {Element} xChartDiv div for the x chart.
+ * @param {Element} yChartDiv div for the y chart.
+ */
+HeatMapChart.prototype.initCharts = function(xChartDiv, yChartDiv) {
+  this.xChart = new google.visualization.SteppedAreaChart(xChartDiv);
+  this.yChart = new google.visualization.SteppedAreaChart(yChartDiv);
+}
+
+/**
+ * This function draw all the chart of the data from eventBuffer.
+ */
+HeatMapChart.prototype.drawCharts = function() {
+  this.xChart.draw(this.eventBuffer.getDataTable('x'),
+      this.getChartOptions('x'));
+  this.yChart.draw(this.eventBuffer.getDataTable('y'),
+      this.getChartOptions('y'));
+}
+
+/**
+ * Process an incoming HeatMapEvent.
+ * @param {object} event
+ */
+HeatMapChart.prototype.processHeatMapEvent = function(event) {
+    this.eventBuffer.appendEvent(event);
+}
+
+/**
+ * Send setPole to backend and select the pole.
+ * @param {object} event
+ */
+function setPole(pole) {
+   window.ws.send('setPole:' + pole);
+   window.console.log('setPole:' + pole);
+   heatMapChart.eventBuffer = new EventBufferWrapper();
+}
+
+/**
+ * Create a web socket and a new TouchLineChart object.
+ */
+function createWS() {
+  var websocket = document.getElementById('websocketUrl').innerText;
+
+  if (window.WebSocket) {
+    ws = new WebSocket(websocket);
+    ws.addEventListener("message", function(event) {
+        var heatMapEvent = JSON.parse(event.data);
+        heatMapChart.processHeatMapEvent(heatMapEvent);
+    });
+  } else {
+    alert('WebSocket is not supported on this browser!')
+  }
+
+  heatMapChart = new HeatMapChart(document.getElementById('xChart'),
+      document.getElementById('yChart'));
+
+  window.setInterval(function() {
+    heatMapChart.drawCharts();
+  }, 33);
+}
+
+/**
+ * Init google chart.
+ */
+function InitChart() {
+  google.charts.load('current', {'packages':['corechart']});
+  google.charts.setOnLoadCallback(createWS);
+}
+
diff --git a/heatmap/heatmapplot.py b/heatmap/heatmapplot.py
new file mode 100755
index 0000000..17e22da
--- /dev/null
+++ b/heatmap/heatmapplot.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python2
+#
+# Copyright 2017 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 launches the cherrypy server for HeatMapPlot and sets up\
+web sockets to handle the messages between the clients and the server.
+
+This module use remote/remote_device to dump heatmap from remote device or local
+device. Currently, this module device with profile sensor. The heatmap is
+reported as array of x readings and y readings. Heatmap data is sent to frontend
+through webscoket. Then javascript will plot the data as bar charts.
+"""
+
+from __future__ import print_function
+from chromite.lib import cros_logging as logging
+import argparse
+import os
+import re
+import subprocess
+import threading
+
+import cherrypy
+
+from ws4py import configure_logger
+from ws4py.messaging import TextMessage
+from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
+from ws4py.websocket import WebSocket
+
+from remote.remote_device import RemoteHeatMapDevice
+
+def SimpleSystem(cmd):
+  """Execute a system command."""
+  ret = subprocess.call(cmd, shell=True)
+  if ret:
+    logging.warning('Command (%s) failed (ret=%s).', cmd, ret)
+
+def SimpleSystemOutput(cmd):
+  """Execute a system command and get its output."""
+  try:
+    proc = subprocess.Popen(
+        cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    stdout, _ = proc.communicate()
+  except Exception, e:
+    logging.warning('Command (%s) failed (%s).', cmd, e)
+  else:
+    return None if proc.returncode else stdout.strip()
+
+
+def IsDestinationPortEnabled(port):
+  """Check if the destination port is enabled in iptables.
+
+  If port 8000 is enabled, it looks like
+    ACCEPT  tcp  --  0.0.0.0/0  0.0.0.0/0  ctstate NEW tcp dpt:8000
+  """
+  pattern = re.compile(r'ACCEPT\s+tcp.+\s+ctstate\s+NEW\s+tcp\s+dpt:%d' % port)
+  rules = SimpleSystemOutput('sudo iptables -L INPUT -n --line-number')
+  for rule in rules.splitlines():
+    if pattern.search(rule):
+      return True
+  return False
+
+
+def EnableDestinationPort(port):
+  """Enable the destination port for input traffic in iptables."""
+  if IsDestinationPortEnabled(port):
+    cherrypy.log('Port %d has been already enabled in iptables.' % port)
+  else:
+    cherrypy.log('Adding a rule to accept incoming connections on port %d in '
+                 'iptables.' % port)
+    cmd = ('sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW '
+           '--dport %d -j ACCEPT' % port)
+    if SimpleSystem(cmd) != 0:
+      raise 'Failed to enable port in iptables: %d.' % port
+
+
+def InterruptHandler():
+  """An interrupt handler for both SIGINT and SIGTERM.
+
+  When interrupt is seen, cherrypy engine will exit.
+  """
+  cherrypy.log('Cherrypy engine exit due to interrupt.')
+  cherrypy.engine.exit()
+
+class HeatMapWSHandler(WebSocket):
+  """The web socket handler for HeatMap."""
+
+  setPoleCallback = None
+  def opened(self):
+    """This method is called when the handler is opened."""
+    cherrypy.log('WS handler is opened!')
+
+  def received_message(self, msg):
+    """A callback for received message."""
+    data = msg.data.split(':', 1)
+    mtype = data[0].lower()
+    content = data[1] if len(data) == 2 else '0'
+
+    if mtype == 'setpole':
+      # code to set pole
+      pole = int(content)
+      cherrypy.log("Setting pole to %d" % pole)
+      if HeatMapWSHandler.setPoleCallback:
+        HeatMapWSHandler.setPoleCallback(pole)
+
+class HeatMapRoot(object):
+  """A class to handle requests about docroot."""
+
+  def __init__(self, ip, port):
+    self.ip = ip
+    self.port = port
+    self.scheme = 'ws'
+    cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
+    cherrypy.log('scheme: %s' % self.scheme)
+
+  @cherrypy.expose
+  def index(self):
+    """This is the default index.html page."""
+    websocket_dict = {
+        'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
+    }
+    root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+                             'heatmap.html')
+    with open(root_page) as f:
+      return f.read() % websocket_dict
+
+  @cherrypy.expose
+  def ws(self):
+    """This handles the request to create a new web socket per client."""
+    cherrypy.log('A new client requesting for WS')
+    cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
+
+class HeatMapPlot(threading.Thread):
+  """The server handling the Plotting of HeatMap."""
+  def __init__(self, server_addr, server_port, device,
+               is_behind_iptables_firewall=False):
+    self._server_addr = server_addr
+    self._server_port = server_port
+    self._device = device
+    super(HeatMapPlot, self).__init__(name='HeatMap thread')
+
+    self.daemon = True
+    self._prev_tids = []
+
+    # Allow input traffic in iptables, if the user has specified.  This setting
+    # should be used if HeatMap is being run directly on a chromebook, but it
+    # requires root access, so we don't want to use it all the time.
+    if is_behind_iptables_firewall:
+      EnableDestinationPort(self._server_port)
+
+    cherrypy.config.update({
+        'server.socket_host': self._server_addr,
+        'server.socket_port': self._server_port,
+    })
+
+    WebSocketPlugin(cherrypy.engine).subscribe()
+    cherrypy.tools.websocket = WebSocketTool()
+
+    # If the cherrypy server exits for whatever reason, close the device
+    # for required cleanup. Otherwise, there might exist local/remote
+    # zombie processes.
+    cherrypy.engine.subscribe('exit', self._device.__del__)
+
+    cherrypy.engine.signal_handler.handlers['SIGINT'] = InterruptHandler
+    cherrypy.engine.signal_handler.handlers['SIGTERM'] = InterruptHandler
+
+  def run(self):
+    """Start the cherrypy engine."""
+    root = HeatMapRoot(self._server_addr, self._server_port)
+
+    cherrypy.quickstart(
+        root,
+        '',
+        config={
+            '/': {
+                'tools.staticdir.root':
+                    os.path.abspath(os.path.dirname(__file__)),
+                'tools.staticdir.on': True,
+                'tools.staticdir.dir': '',
+            },
+            '/ws': {
+                'tools.websocket.on': True,
+                'tools.websocket.handler_cls': HeatMapWSHandler,
+            },
+        }
+    )
+
+  def StartBridgeHeatMapEvents(self):
+    """Start to read heatmap events and send it over ws."""
+    cherrypy.log('Start to route heatmap events')
+
+    try:
+      while True:
+        event = self._device.NextEvent()
+        if event:
+          cherrypy.engine.publish('websocket-broadcast', event.ToJson())
+        else:
+          cherrypy.log('Fail to get heatmap event, check if heatmap_tool is on\
+your device.')
+          cherrypy.engine.exit()
+    except KeyboardInterrupt:
+      cherrypy.log('Keyboard Interrupt accepted')
+      cherrypy.log('HeatMapPlot is being terminated')
+      cherrypy.engine.exit()
+
+def _CheckLegalUser():
+  """If this program is run in chroot, it should not be run as root for\
+security reason."""
+  if os.path.exists('/etc/cros_chroot_version') and os.getuid() == 0:
+    print ('You should run heatmapplot in chroot as a regular user '
+           'instead of as root.\n')
+    exit(1)
+
+
+def _ParseArguments():
+  """Parse the command line options."""
+  parser = argparse.ArgumentParser(description='HeatMap Server')
+  parser.add_argument('-d', '--dut_addr', default=None,
+                      help='the address of the dut')
+  parser.add_argument('-p', '--server_port', default=8080, type=int,
+                      help='the port the web server listens to (default: 8080)')
+  parser.add_argument('--behind_firewall', action='store_true',
+                      help=('With this flag set, you tell webplot to add a '
+                            'rule to iptables to allow incoming traffic to '
+                            'the webserver.  If you are running HeatMapPlot on '
+                            'a chromebook, this is needed.'))
+  parser.add_argument('-s', '--server_addr', default='127.0.0.1',
+                      help='the address the webplot http server listens to')
+  args = parser.parse_args()
+  return args
+
+
+def Main():
+  """The main function to launch HeatMap service."""
+  _CheckLegalUser()
+
+  configure_logger(level=logging.ERROR)
+  args = _ParseArguments()
+
+  print('\n' + '-' * 70)
+  cherrypy.log('dut address: %s' % args.dut_addr)
+  cherrypy.log('web server address: %s' % args.server_addr)
+  cherrypy.log('web server port: %s' % args.server_port)
+  print('-' * 70 + '\n\n')
+
+  if args.server_port == 80:
+    url = 'http://%s' % args.server_addr
+  else:
+    url = 'http://%s:%d' % (args.server_addr, args.server_port)
+
+  msg = 'Type "%s" in browser %s to see heatmap.\n'
+  if args.server_addr == '127.0.0.1':
+    which_machine = 'on the webplot server machine'
+  else:
+    which_machine = 'on any machine'
+
+  print('*' * 70)
+  print(msg % (url, which_machine))
+  print('*' * 70 + '\n\n')
+
+  # Instantiate a touch device.
+  addr = args.dut_addr if args.dut_addr else '127.0.0.1'
+
+  device = RemoteHeatMapDevice(addr)
+
+  # Setup the call back function to set pole to device.
+  HeatMapWSHandler.setPoleCallback = device.SetPole
+
+  # Instantiate a webplot server daemon and start it.
+  HeatMap = HeatMapPlot(args.server_addr, args.server_port, device,
+                        is_behind_iptables_firewall=args.behind_firewall)
+
+  HeatMap.start()
+  HeatMap.StartBridgeHeatMapEvents()
+
+if __name__ == '__main__':
+  Main()
diff --git a/heatmap/remote/__init__.py b/heatmap/remote/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/heatmap/remote/__init__.py
diff --git a/heatmap/remote/data b/heatmap/remote/data
new file mode 120000
index 0000000..2b5444c
--- /dev/null
+++ b/heatmap/remote/data
@@ -0,0 +1 @@
+../../remote/data
\ No newline at end of file
diff --git a/heatmap/remote/hidraw/__init__.py b/heatmap/remote/hidraw/__init__.py
new file mode 100644
index 0000000..f3d9416
--- /dev/null
+++ b/heatmap/remote/hidraw/__init__.py
@@ -0,0 +1,5 @@
+# Copyright 2017 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.
+
+from hidraw import *
diff --git a/heatmap/remote/hidraw/hidraw.py b/heatmap/remote/hidraw/hidraw.py
new file mode 100644
index 0000000..a053a24
--- /dev/null
+++ b/heatmap/remote/hidraw/hidraw.py
@@ -0,0 +1,142 @@
+# Copyright 2017 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 includes the python hidraw libray
+
+The hidraw interface is a straight forward translation of linux hidraw c
+interface. Running this file directly, it will print all the hidraw device.
+"""
+
+from __future__ import print_function
+import array
+import ctypes
+import fcntl
+import glob
+from input.linux_ioctl import IOC, IOC_READ, IOC_WRITE
+
+HID_MAX_DESCRIPTOR_SIZE = 4096
+
+class HIDRawReportDescriptor(ctypes.Structure):
+  """A class for linux hidraw_report_descriptor
+
+  struct hidraw_report_descriptor {
+    __u32 size;
+    __u8 value[HID_MAX_DESCRIPTOR_SIZE];
+  }
+  """
+
+  _fields_ = [
+      ('size', ctypes.c_uint),
+      ('value', ctypes.c_ubyte * HID_MAX_DESCRIPTOR_SIZE),
+  ]
+
+
+class HIDRawDevInfo(ctypes.Structure):
+  """A class for linux hidraw_devinfo
+
+  struct hidraw_devinfo {
+    __u32 bustype;
+    __s16 vendor;
+    __s16 product;
+  }
+  """
+
+  _fields_ = [
+      ('bustype', ctypes.c_uint),
+      ('vendor', ctypes.c_ushort),
+      ('product', ctypes.c_ushort),
+  ]
+
+
+# The following code are from hidraw.h
+
+HIDIOCGRDESCSIZE = IOC(IOC_READ, 'H', 0x01, ctypes.sizeof(ctypes.c_uint))
+HIDIOCGRDESC = IOC(IOC_READ, 'H', 0x02,
+                   ctypes.sizeof(HIDRawReportDescriptor))
+HIDIOCGRAWINFO = IOC(IOC_READ, 'H', 0x03, ctypes.sizeof(HIDRawDevInfo))
+
+def HIDIOCGRAWNAME(length):
+  return IOC(IOC_READ, 'H', 0x04, length)
+
+def HIDIOCGRAWPHYS(length):
+  return IOC(IOC_READ, 'H', 0x05, length)
+
+def HIDIOCSFEATURE(length):
+  return IOC(IOC_READ | IOC_WRITE, 'H', 0x06, length)
+
+def HIDIOCGFEATURE(length):
+  return IOC(IOC_READ | IOC_WRITE, 'H', 0x07, length)
+
+HIDRAW_FIRST_MINOR = 0
+HIDRAW_MAX_DEVICES = 64
+HIDRAW_BUFFER_SIZE = 64
+
+
+class HIDRaw(object):
+  """A class used to access hidraw interface."""
+
+  def __init__(self, path):
+    self.path = path
+    self.f = open(path, 'rb+', buffering=0)
+    self._ioctl_raw_report_descritpor()
+    self._ioctl_info()
+    self._ioctl_name()
+    self._ioctl_physical_addr()
+
+  def __exit__(self, *_):
+    if self.f and not self.f.closed:
+      self.f.close()
+
+  def send_feature_report(self, report_buffer):
+    fcntl.ioctl(self.f, HIDIOCSFEATURE(len(report_buffer)), report_buffer, 1)
+
+  def get_feature_report(self, report_num, length):
+    report_buffer = array.array('B', [0] * length)
+    report_buffer[0] = report_num
+    fcntl.ioctl(self.f, HIDIOCGFEATURE(length), report_buffer, 1)
+    return buffer
+
+  def read(self, size):
+    return self.f.read(size)
+
+  def _ioctl_raw_report_descritpor(self):
+    """Queries device file for the report descriptor"""
+    descriptor_size = ctypes.c_uint()
+    fcntl.ioctl(self.f, HIDIOCGRDESCSIZE, descriptor_size, 1)
+
+    self.descriptor = HIDRawReportDescriptor()
+    self.descriptor.size = descriptor_size
+    fcntl.ioctl(self.f, HIDIOCGRDESC, self.descriptor, 1)
+
+  def _ioctl_info(self):
+    """Queries device file for the dev info"""
+    self.devinfo = HIDRawDevInfo()
+    fcntl.ioctl(self.f, HIDIOCGRAWINFO, self.devinfo, 1)
+
+  def _ioctl_name(self):
+    """Queries device file for the dev name"""
+    name_len = 255
+    name = array.array('B', [0] * name_len)
+    name_len = fcntl.ioctl(self.f, HIDIOCGRAWNAME(name_len), name, 1)
+    self.name = name[0:name_len-1].tostring()
+
+  def _ioctl_physical_addr(self):
+    """Queries device file for the dev physical addr"""
+    addr_len = 255
+    addr = array.array('B', [0] * addr_len)
+    addr_len = fcntl.ioctl(self.f, HIDIOCGRAWPHYS(addr_len), addr, 1)
+    self.physic_addr = addr[0:addr_len-1].tostring()
+
+
+def main():
+  """Function to interactively select a hidraw device"""
+  for path in glob.glob('/dev/hidraw*'):
+    hid = HIDRaw(path)
+    print('Device at:', path)
+    print('   ', hid.physic_addr, ':', hid.name)
+    print('   ', "Vendor:", hex(hid.devinfo.vendor))
+    print('   ', 'Product:', hex(hid.devinfo.product))
+
+if __name__ == '__main__':
+  main()
diff --git a/heatmap/remote/hidraw/input b/heatmap/remote/hidraw/input
new file mode 120000
index 0000000..98c0e29
--- /dev/null
+++ b/heatmap/remote/hidraw/input
@@ -0,0 +1 @@
+../../../remote/mt/input/
\ No newline at end of file
diff --git a/heatmap/remote/remote_device.py b/heatmap/remote/remote_device.py
new file mode 100644
index 0000000..78d6f2b
--- /dev/null
+++ b/heatmap/remote/remote_device.py
@@ -0,0 +1,170 @@
+# Copyright 2017 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.
+
+"""Remote classes for heatmap devices on full DUTs being tested.
+
+This module includes RemoteHeatMapDevice. It can be used to call remote command
+on DUT. RemoteHeatMapDevice use hetamap_tool(device specific driver) as backend.
+RemoteHeatMapDevice.NextEvent will get the next heatmap event from remote
+device. Currently, it only support heatmap from profile sensor. The heatmap
+event is array of x sensor reading or y sensor reading.
+"""
+
+from __future__ import print_function
+import inspect
+import os
+import select
+import stat
+import sys
+import json
+from subprocess import PIPE, Popen
+
+class HeatMapEvent(object):
+  """Container class for heatmap event."""
+
+  def __init__(self, sensor, data):
+    self.sensor = sensor
+    self.data = data
+
+  def ToJson(self):
+    return json.dumps({'sensor':self.sensor, 'data':self.data})
+
+
+class RemoteHeatMapDevice(object):
+  """This class represents a remote heatmap device.
+
+  Connect to a remote device by creating an instance with addr. Then call
+  NextEvent will get the next heatmap event.
+  """
+
+  def __init__(self, addr):
+    self.pole = 0
+    self.restart_flag = False
+    self.addr = addr
+    self.begin_event_stream_cmd = 'heatmap_tool --action dump --pole %s'
+    self.kill_remote_process_cmd = 'heatmap_tool --action reset_device'
+    self.event_stream_process = None
+
+    # Spawn the event gathering subprocess
+    self._InitializeEventGatheringSubprocess()
+    self._RunRemoteCmd('stop powerd')
+
+  def SetPole(self, pole):
+    if self.pole != pole:
+      self.pole = pole
+      self.restart_flag = True
+
+  def Restart(self):
+    self._StopStreamOfEvents()
+    self._InitializeEventGatheringSubprocess()
+    self.restart_flag = False
+
+  def _InitializeEventGatheringSubprocess(self):
+    # Initiate the streaming connection
+    print('Run dump heatmap with pole %s' % self.pole)
+    self.event_stream_process = self._RunRemoteCmd(
+        self.begin_event_stream_cmd % self.pole)
+
+    # Check to make sure it didn't terminate immediately
+    ret_code = self.event_stream_process.poll()
+    if ret_code is not None:
+      print('ERROR: streaming terminated unexpectedly (%d)' % ret_code)
+      return False
+
+    # Block until there's *something* to read, indicating everything is ready
+    readable, _, _, = select.select([self.event_stream_process.stdout], [], [])
+    return self.event_stream_process.stdout in readable
+
+  def NextEvent(self, timeout=None):
+    """Wait for and capture the next heatmap event."""
+    if self.restart_flag:
+      self.Restart()
+
+    event = None
+    while not event:
+      if not self.event_stream_process:
+        return None
+
+      line = self._GetNextLine(timeout)
+      if not line:
+        return None
+
+      event = self._ParseHeatMap(line)
+    return event
+
+  def _GetNextLine(self, timeout=None):
+    if timeout:
+      inputs = [self.event_stream_process.stdout]
+      readable, _, _, = select.select(inputs, [], [], timeout)
+      if inputs[0] not in readable:
+        return None
+
+    line = self.event_stream_process.stdout.readline()
+
+    # If the event_stream_process had been terminated, just return None.
+    if self.event_stream_process is None:
+      return None
+
+    if line == '' and self.event_stream_process.poll() != None:
+      self.event_stream_process = None
+      return None
+    return line
+
+  def _ParseHeatMap(self, line):
+    """Parse the heatmap event.
+
+    HeatMap look like this:
+    x 2704.52347057 3439.14661528 3213.1087246 3043.58030659
+    """
+    items = line.split()
+    if len(items) < 1 or (items[0] != 'x' and items[0] != 'y'):
+      return None
+    data = map(float, items[1:])
+    return HeatMapEvent(items[0], data)
+
+  def _RunRemoteCmd(self, cmd):
+    """Run a command on the shell of a remote ChromeOS DUT."""
+    RSA_KEY_PATH = os.path.dirname(
+        os.path.realpath(inspect.getfile(
+            inspect.currentframe()))) + '/data/testing_rsa'
+    if stat.S_IMODE(os.stat(RSA_KEY_PATH).st_mode) != 0600:
+      os.chmod(RSA_KEY_PATH, 0600)
+
+    args = ['ssh', 'root@%s' % self.addr,
+            '-i', RSA_KEY_PATH,
+            '-o', 'UserKnownHostsFile=/dev/null',
+            '-o', 'StrictHostKeyChecking=no',
+            cmd]
+    return Popen(args, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+
+  def _StopStreamOfEvents(self):
+    """Stops the stream of events.
+
+    There are two steps.
+    Step 1: Kill the remote process; otherwise, it becomes a zombie process.
+    Step 2: Kill the local ssh subprocess.
+            This terminates the subprocess that's maintaining the connection
+            with the DUT and returns its return code.
+    """
+    # Step 1: kill the remote process; otherwise, it becomes a zombie process.
+    killing_process = self._RunRemoteCmd(self.kill_remote_process_cmd)
+    killing_process.wait()
+
+    # Step 2: Kill the local ssh/adb subprocess.
+    # If self.event_stream_process has been terminated, its value is None.
+    if self.event_stream_process is None:
+      return None
+
+    # Kill the subprocess if it is still alive with return_code as None.
+    return_code = self.event_stream_process.poll()
+    if return_code is None:
+      self.event_stream_process.terminate()
+      return_code = self.event_stream_process.wait()
+      if return_code is None:
+        print('Error in killing the event_stream_process!')
+    self.event_stream_process = None
+    return return_code
+
+  def __del__(self):
+    self._StopStreamOfEvents()
diff --git a/setup.py b/setup.py
index 983a6b1..9767d4a 100755
--- a/setup.py
+++ b/setup.py
@@ -13,10 +13,16 @@
       packages=['webplot',
                 'webplot.remote',
                 'webplot.remote.mt',
-                'webplot.remote.mt.input'],
+                'webplot.remote.mt.input',
+                'heatmap',
+                'heatmap.remote',
+                'heatmap.remote.hidraw',
+                'heatmap.remote.hidraw.input'],
       package_data={'webplot': ['*.html', '*.js', 'webplot', 'linechart/*.js',
                                 'linechart/*.html'],
-                    'webplot.remote': ['data/*',]},
+                    'webplot.remote': ['data/*',],
+                    'heatmap': ['*.html', '*.js'],
+                    'heatmap.remote': ['data/*',]},
       author='Joseph Hwang',
       author_email='josephsih@chromium.org',
 )