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',
)