blob: 12b1ecd3f0e83854d90e024d556f3674d3303430 [file] [log] [blame]
Charlie Mooneybbc05f52015-03-24 13:36:22 -07001# Copyright (c) 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""This module launches the cherrypy server for webplot and sets up
6web sockets to handle the messages between the clients and the server.
7"""
8
9import argparse
10import base64
11import json
12import logging
13import os
Charlie Mooneybf469942015-07-09 11:21:12 -070014import pwd
Charlie Mooneybbc05f52015-03-24 13:36:22 -070015import re
16import subprocess
Charlie Mooneyc68f9c32015-04-16 15:23:22 -070017import time
Charlie Mooneybbc05f52015-03-24 13:36:22 -070018import threading
Charlie Mooney54e2f2e2015-07-09 10:53:38 -070019import webbrowser
Charlie Mooneybbc05f52015-03-24 13:36:22 -070020
21import cherrypy
22
Charlie Mooneybbc05f52015-03-24 13:36:22 -070023from ws4py import configure_logger
24from ws4py.messaging import TextMessage
25from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
26from ws4py.websocket import WebSocket
27
Charlie Mooney04b41532015-04-02 12:41:37 -070028from remote import ChromeOSTouchDevice, AndroidTouchDevice
Charlie Mooneybbc05f52015-03-24 13:36:22 -070029
30
31# The WebSocket connection state object.
32state = None
33
34# The touch events are saved in this file as default.
Charlie Mooneybf469942015-07-09 11:21:12 -070035current_username = pwd.getpwuid(os.getuid()).pw_name
36SAVED_FILE = '/tmp/webplot_%s.dat' % current_username
37SAVED_IMAGE = '/tmp/webplot_%s.png' % current_username
Charlie Mooneybbc05f52015-03-24 13:36:22 -070038
39
Joseph Hwang4782a042015-04-08 17:15:50 +080040def SimpleSystem(cmd):
41 """Execute a system command."""
42 ret = subprocess.call(cmd, shell=True)
43 if ret:
44 logging.warning('Command (%s) failed (ret=%s).', cmd, ret)
45 return ret
46
47
48def SimpleSystemOutput(cmd):
49 """Execute a system command and get its output."""
50 try:
51 proc = subprocess.Popen(
52 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
53 stdout, _ = proc.communicate()
54 except Exception, e:
55 logging.warning('Command (%s) failed (%s).', cmd, e)
56 else:
57 return None if proc.returncode else stdout.strip()
58
59
60def IsDestinationPortEnabled(port):
61 """Check if the destination port is enabled in iptables.
62
63 If port 8000 is enabled, it looks like
64 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW tcp dpt:8000
65 """
66 pattern = re.compile('ACCEPT\s+tcp.+\s+ctstate\s+NEW\s+tcp\s+dpt:%d' % port)
67 rules = SimpleSystemOutput('sudo iptables -L INPUT -n --line-number')
68 for rule in rules.splitlines():
69 if pattern.search(rule):
70 return True
71 return False
72
73
74def EnableDestinationPort(port):
75 """Enable the destination port for input traffic in iptables."""
76 if IsDestinationPortEnabled(port):
77 cherrypy.log('Port %d has been already enabled in iptables.' % port)
78 else:
Charlie Mooneye15a5552015-07-10 13:48:03 -070079 cherrypy.log('Adding a rule to accept incoming connections on port %d in '
80 'iptables.' % port)
Joseph Hwang4782a042015-04-08 17:15:50 +080081 cmd = ('sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW '
82 '--dport %d -j ACCEPT' % port)
83 if SimpleSystem(cmd) != 0:
84 raise Error('Failed to enable port in iptables: %d.' % port)
85
86
Charlie Mooneybbc05f52015-03-24 13:36:22 -070087def InterruptHandler():
88 """An interrupt handler for both SIGINT and SIGTERM
89
90 The stop procedure triggered is as follows:
91 1. This handler sends a 'quit' message to the listening client.
92 2. The client sends the canvas image back to the server in its quit message.
93 3. WebplotWSHandler.received_message() saves the image.
94 4. WebplotWSHandler.received_message() handles the 'quit' message.
95 The cherrypy engine exits if this is the last client.
96 """
97 cherrypy.log('Cherrypy engine is sending quit message to clients.')
Joseph Hwang95bf52a2015-04-14 13:09:39 +080098 state.QuitAndShutdown()
99
100
101def _IOError(e, filename):
102 err_msg = ['\n', '!' * 60, str(e),
103 'It is likely that %s is owned by root.' % filename,
104 'Please remove the file and then run webplot again.',
105 '!' * 60, '\n']
106 cherrypy.log('\n'.join(err_msg))
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700107
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700108image_lock = threading.Event()
109image_string = ''
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700110
111class WebplotWSHandler(WebSocket):
112 """The web socket handler for webplot."""
113
114 def opened(self):
115 """This method is called when the handler is opened."""
116 cherrypy.log('WS handler is opened!')
117
118 def received_message(self, msg):
119 """A callback for received message."""
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700120 data = msg.data.split(':', 1)
121 mtype = data[0].lower()
122 content = data[1] if len(data) == 2 else None
Joseph Hwang0c1fa7d2015-04-09 17:01:45 +0800123
124 # Do not print the image data since it is too large.
125 if mtype != 'save':
126 cherrypy.log('Received message: %s' % str(msg.data))
127
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700128 if mtype == 'quit':
129 # A shutdown message requested by the user.
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700130 cherrypy.log('The user requests to shutdown the cherrypy server....')
131 state.DecCount()
132 elif mtype == 'save':
Charlie Mooneyb476e892015-04-02 13:25:49 -0700133 cherrypy.log('All data saved to "%s"' % SAVED_FILE)
134 self.SaveImage(content, SAVED_IMAGE)
135 cherrypy.log('Plot image saved to "%s"' % SAVED_IMAGE)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700136 else:
137 cherrypy.log('Unknown message type: %s' % mtype)
138
139 def closed(self, code, reason="A client left the room."):
140 """This method is called when the handler is closed."""
141 cherrypy.log('A client requests to close WS.')
142 cherrypy.engine.publish('websocket-broadcast', TextMessage(reason))
143
144 @staticmethod
145 def SaveImage(image_data, image_file):
146 """Decoded the base64 image data and save it in the file."""
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700147 global image_string
148 image_string = base64.b64decode(image_data)
149 image_lock.set()
Joseph Hwangafc092c2015-04-21 11:32:45 +0800150 try:
151 with open(image_file, 'w') as f:
152 f.write(image_string)
153 except IOError as e:
154 _IOError(e, image_file)
155 state.QuitAndShutdown()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700156
157
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700158class ConnectionState(object):
159 """A ws connection state object for shutting down the cherrypy server.
160
161 It shuts down the cherrypy server when the count is down to 0 and is not
162 increased before the shutdown_timer expires.
163
164 Note that when a page refreshes, it closes the WS connection first and
165 then re-connects immediately. This is why we would like to wait a while
166 before actually shutting down the server.
167 """
168 TIMEOUT = 1.0
169
170 def __init__(self):
171 self.count = 0;
172 self.lock = threading.Lock()
173 self.shutdown_timer = None
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800174 self.quit_flag = False
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700175
176 def IncCount(self):
177 """Increase the connection count, and cancel the shutdown timer if exists.
178 """
179 self.lock.acquire()
180 self.count += 1;
181 cherrypy.log(' WS connection count: %d' % self.count)
182 if self.shutdown_timer:
183 self.shutdown_timer.cancel()
184 self.shutdown_timer = None
185 self.lock.release()
186
187 def DecCount(self):
188 """Decrease the connection count, and start a shutdown timer if no other
189 clients are connecting to the server.
190 """
191 self.lock.acquire()
192 self.count -= 1;
193 cherrypy.log(' WS connection count: %d' % self.count)
194 if self.count == 0:
195 self.shutdown_timer = threading.Timer(self.TIMEOUT, self.Shutdown)
196 self.shutdown_timer.start()
197 self.lock.release()
198
Joseph Hwang3f561d32015-04-09 16:33:56 +0800199 def ShutdownWhenNoConnections(self):
200 """Shutdown cherrypy server when there is no client connection."""
201 self.lock.acquire()
202 if self.count == 0 and self.shutdown_timer is None:
203 self.Shutdown()
204 self.lock.release()
205
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700206 def Shutdown(self):
207 """Shutdown the cherrypy server."""
208 cherrypy.log('Shutdown timer expires. Cherrypy server for Webplot exits.')
209 cherrypy.engine.exit()
210
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800211 def QuitAndShutdown(self):
212 """The server notifies clients to quit and then shuts down."""
213 if not self.quit_flag:
214 self.quit_flag = True
215 cherrypy.engine.publish('websocket-broadcast', TextMessage('quit'))
216 self.ShutdownWhenNoConnections()
217
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700218
Johny Lin908b92a2015-08-27 21:59:30 +0800219class TouchRoot(object):
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700220 """A class to handle requests about docroot."""
221
222 def __init__(self, ip, port, touch_min_x, touch_max_x, touch_min_y,
Jingkui Wang55115ef2017-06-17 13:44:33 -0700223 touch_max_y, touch_min_pressure, touch_max_pressure,
Jingkui Wangbafba932018-05-03 11:16:19 -0700224 tilt_min_x, tilt_max_x, tilt_min_y, tilt_max_y,
225 major_min, major_max, minor_min, minor_max):
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700226 self.ip = ip
227 self.port = port
228 self.touch_min_x = touch_min_x
229 self.touch_max_x = touch_max_x
230 self.touch_min_y = touch_min_y
231 self.touch_max_y = touch_max_y
232 self.touch_min_pressure = touch_min_pressure
233 self.touch_max_pressure = touch_max_pressure
Jingkui Wang55115ef2017-06-17 13:44:33 -0700234 self.tilt_min_x = tilt_min_x
235 self.tilt_max_x = tilt_max_x
236 self.tilt_min_y = tilt_min_y
237 self.tilt_max_y = tilt_max_y
Jingkui Wangbafba932018-05-03 11:16:19 -0700238 self.major_min = major_min
239 self.major_max = major_max
240 self.minor_min = minor_min
241 self.minor_max = minor_max
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700242 self.scheme = 'ws'
243 cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
244 cherrypy.log('scheme: %s' % self.scheme)
245
246 @cherrypy.expose
247 def index(self):
248 """This is the default index.html page."""
249 websocket_dict = {
250 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
251 'touchMinX': str(self.touch_min_x),
252 'touchMaxX': str(self.touch_max_x),
253 'touchMinY': str(self.touch_min_y),
254 'touchMaxY': str(self.touch_max_y),
255 'touchMinPressure': str(self.touch_min_pressure),
256 'touchMaxPressure': str(self.touch_max_pressure),
Jingkui Wang55115ef2017-06-17 13:44:33 -0700257 'tiltMinX': str(self.tilt_min_x),
258 'tiltMaxX': str(self.tilt_max_x),
259 'tiltMinY': str(self.tilt_min_y),
260 'tiltMaxY': str(self.tilt_max_y),
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700261 }
262 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
263 'webplot.html')
264 with open(root_page) as f:
265 return f.read() % websocket_dict
266
267 @cherrypy.expose
Jingkui Wang7ed915f2017-06-22 17:31:54 -0700268 def linechart(self):
269 """This is the default linechart.html page."""
270 websocket_dict = {
271 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
272 'touchMinX': str(self.touch_min_x),
273 'touchMaxX': str(self.touch_max_x),
274 'touchMinY': str(self.touch_min_y),
275 'touchMaxY': str(self.touch_max_y),
276 'touchMinPressure': str(self.touch_min_pressure),
277 'touchMaxPressure': str(self.touch_max_pressure),
278 'tiltMinX': str(self.tilt_min_x),
279 'tiltMaxX': str(self.tilt_max_x),
280 'tiltMinY': str(self.tilt_min_y),
281 'tiltMaxY': str(self.tilt_max_y),
Jingkui Wangbafba932018-05-03 11:16:19 -0700282 'majorMin': str(self.major_min),
283 'majorMax': str(self.major_max),
284 'minorMin': str(self.minor_min),
285 'minorMax': str(self.minor_max),
Jingkui Wang7ed915f2017-06-22 17:31:54 -0700286 }
287 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
288 'linechart/linechart.html')
289 with open(root_page) as f:
290 return f.read() % websocket_dict
291
292 @cherrypy.expose
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700293 def ws(self):
294 """This handles the request to create a new web socket per client."""
295 cherrypy.log('A new client requesting for WS')
296 cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
297 state.IncCount()
298
299
Johny Lin908b92a2015-08-27 21:59:30 +0800300class CentroidingRoot(object):
301 """A class to handle requests about docroot."""
302
303 def __init__(self, ip, port, data_scale, data_offset,
304 data_width, data_height):
305 self.ip = ip
306 self.port = port
307 self.data_scale = data_scale
308 self.data_offset = data_offset
309 self.data_width = data_width
310 self.data_height = data_height
311 self.scheme = 'ws'
312 cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
313 cherrypy.log('scheme: %s' % self.scheme)
314
315 @cherrypy.expose
316 def index(self):
317 """This is the default index.html page."""
318 websocket_dict = {
319 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
320 'dataScale': str(self.data_scale),
321 'dataOffset': str(self.data_offset),
322 'dataWidth': str(self.data_width),
323 'dataHeight': str(self.data_height),
324 }
325 print websocket_dict
326 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
327 'centroiding.html')
328 with open(root_page) as f:
329 return f.read() % websocket_dict
330
331 @cherrypy.expose
332 def ws(self):
333 """This handles the request to create a new web socket per client."""
334 cherrypy.log('A new client requesting for WS')
335 cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
336 state.IncCount()
337
338
Joseph Hwang4782a042015-04-08 17:15:50 +0800339class Webplot(threading.Thread):
340 """The server handling the Plotting of finger traces.
341
342 Use case 1: embedding Webplot as a plotter in an application
343
344 # Instantiate a webplot server and starts the daemon.
345 plot = Webplot(server_addr, server_port, device)
346 plot.start()
347
348 # Repeatedly get a snapshot and add it for plotting.
349 while True:
350 # GetSnapshot() is essentially device.NextSnapshot()
351 snapshot = plot.GetSnapshot()
352 if not snapshot:
353 break
354 # Add the snapshot to the plotter for plotting.
355 plot.AddSnapshot(snapshot)
356
357 # Save a screen dump
358 plot.Save()
359
360 # Notify the browser to clear the screen.
361 plot.Clear()
362
363 # Notify both the browser and the cherrypy engine to quit.
364 plot.Quit()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700365
366
Joseph Hwang4782a042015-04-08 17:15:50 +0800367 Use case 2: using webplot standalone
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700368
Joseph Hwang4782a042015-04-08 17:15:50 +0800369 # Instantiate a webplot server and starts the daemon.
370 plot = Webplot(server_addr, server_port, device)
371 plot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700372
Joseph Hwang4782a042015-04-08 17:15:50 +0800373 # Get touch snapshots from the touch device and have clients plot them.
374 webplot.GetAndPlotSnapshots()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700375 """
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700376
Joseph Hwange9cfd642015-04-20 16:00:33 +0800377 def __init__(self, server_addr, server_port, device, saved_file=SAVED_FILE,
Johny Lin908b92a2015-08-27 21:59:30 +0800378 logging=False, is_behind_iptables_firewall=False,
379 is_centroiding=False):
Joseph Hwang4782a042015-04-08 17:15:50 +0800380 self._server_addr = server_addr
381 self._server_port = server_port
382 self._device = device
383 self._saved_file = saved_file
Johny Lin908b92a2015-08-27 21:59:30 +0800384 self._is_centroiding = is_centroiding
Joseph Hwang4782a042015-04-08 17:15:50 +0800385 super(Webplot, self).__init__(name='webplot thread')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700386
Joseph Hwang4782a042015-04-08 17:15:50 +0800387 self.daemon = True
388 self._prev_tids = []
389
Joseph Hwange9cfd642015-04-20 16:00:33 +0800390 # The logging is turned off by default when imported as a module so that
391 # it does not mess up the screen.
392 if not logging:
393 cherrypy.log.screen = None
394
Charlie Mooneye15a5552015-07-10 13:48:03 -0700395 # Allow input traffic in iptables, if the user has specified. This setting
396 # should be used if webplot is being run directly on a chromebook, but it
397 # requires root access, so we don't want to use it all the time.
398 if is_behind_iptables_firewall:
399 EnableDestinationPort(self._server_port)
Joseph Hwang4782a042015-04-08 17:15:50 +0800400
401 # Create a ws connection state object to wait for the condition to
402 # shutdown the whole process.
403 global state
404 state = ConnectionState()
405
406 cherrypy.config.update({
407 'server.socket_host': self._server_addr,
408 'server.socket_port': self._server_port,
409 })
410
411 WebSocketPlugin(cherrypy.engine).subscribe()
412 cherrypy.tools.websocket = WebSocketTool()
413
414 # If the cherrypy server exits for whatever reason, close the device
415 # for required cleanup. Otherwise, there might exist local/remote
416 # zombie processes.
Johny Lin908b92a2015-08-27 21:59:30 +0800417 if not self._is_centroiding:
418 cherrypy.engine.subscribe('exit', self._device.__del__)
Joseph Hwang4782a042015-04-08 17:15:50 +0800419
420 cherrypy.engine.signal_handler.handlers['SIGINT'] = InterruptHandler
421 cherrypy.engine.signal_handler.handlers['SIGTERM'] = InterruptHandler
422
423 def run(self):
424 """Start the cherrypy engine."""
Johny Lin908b92a2015-08-27 21:59:30 +0800425 if not self._is_centroiding:
426 x_min, x_max = self._device.RangeX()
427 y_min, y_max = self._device.RangeY()
428 p_min, p_max = self._device.RangeP()
Jingkui Wang55115ef2017-06-17 13:44:33 -0700429 tilt_x_min, tilt_x_max = self._device.RangeTiltX()
430 tilt_y_min, tilt_y_max = self._device.RangeTiltY()
Jingkui Wangbafba932018-05-03 11:16:19 -0700431 major_min, major_max = self._device.RangeMajor()
432 minor_min, minor_max = self._device.RangeMinor()
433
Johny Lin908b92a2015-08-27 21:59:30 +0800434 root = TouchRoot(self._server_addr, self._server_port,
Jingkui Wang55115ef2017-06-17 13:44:33 -0700435 x_min, x_max, y_min, y_max, p_min, p_max, tilt_x_min,
Jingkui Wangbafba932018-05-03 11:16:19 -0700436 tilt_x_max, tilt_y_min, tilt_y_max,
437 major_min, major_max, minor_min, minor_max,)
Johny Lin908b92a2015-08-27 21:59:30 +0800438 else:
439 data_scale = self._device.data_scale
440 data_offset = self._device.data_offset
441 data_width = self._device.width
442 data_height = self._device.height
Jingkui Wang55115ef2017-06-17 13:44:33 -0700443 tilt_x_min, tilt_x_max = self._device.RangeTiltX()
444 tilt_y_min, tilt_y_max = self._device.RangeTiltY()
Johny Lin908b92a2015-08-27 21:59:30 +0800445 root = CentroidingRoot(self._server_addr, self._server_port,
Jingkui Wang55115ef2017-06-17 13:44:33 -0700446 data_scale, data_offset, data_width, data_height,
447 tilt_x_min, tilt_x_max, tilt_y_min, tilt_y_max)
Joseph Hwang4782a042015-04-08 17:15:50 +0800448
449 cherrypy.quickstart(
Johny Lin908b92a2015-08-27 21:59:30 +0800450 root,
Joseph Hwang4782a042015-04-08 17:15:50 +0800451 '',
452 config={
453 '/': {
454 'tools.staticdir.root':
455 os.path.abspath(os.path.dirname(__file__)),
456 'tools.staticdir.on': True,
457 'tools.staticdir.dir': '',
458 },
459 '/ws': {
460 'tools.websocket.on': True,
461 'tools.websocket.handler_cls': WebplotWSHandler,
462 },
463 }
464 )
465
466 def _ConvertNamedtupleToDict(self, snapshot):
467 """Convert namedtuples to ordinary dictionaries and add leaving slots.
468
469 This is to make a snapshot json serializable. Otherwise, the namedtuples
470 would be transmitted as arrays which is less readable.
471
472 A snapshot looks like
473 MtSnapshot(
474 syn_time=1420524008.368854,
475 button_pressed=False,
Jingkui Wangcfabdd52017-06-27 10:28:20 -0700476 fingers=[
477 MtFinger(tid=162, slot=0, syn_time=1420524008.368854, x=524,
Jingkui Wang55115ef2017-06-17 13:44:33 -0700478 y=231, pressure=45, tilt_x=0, tilt_y=0),
Jingkui Wangcfabdd52017-06-27 10:28:20 -0700479 MtFinger(tid=163, slot=1, syn_time=1420524008.368854, x=677,
Jingkui Wang55115ef2017-06-17 13:44:33 -0700480 y=135, pressure=57, tilt_x=0, tilt_y=0)
Joseph Hwang4782a042015-04-08 17:15:50 +0800481 ]
482 )
483
484 Note:
485 1. that there are two levels of namedtuples to convert.
486 2. The leaving slots are used to notify javascript that a finger is leaving
487 so that the corresponding finger color could be released for reuse.
488 """
489 # Convert MtSnapshot.
490 converted = dict(snapshot.__dict__.items())
491
Jingkui Wangcfabdd52017-06-27 10:28:20 -0700492 # Convert MtFinger.
493 converted['fingers'] = [dict(finger.__dict__.items())
494 for finger in converted['fingers']]
Joseph Hwang4782a042015-04-08 17:15:50 +0800495 converted['raw_events'] = [str(event) for event in converted['raw_events']]
496
497 # Add leaving fingers to notify js for reclaiming the finger colors.
Jingkui Wangcfabdd52017-06-27 10:28:20 -0700498 curr_tids = [finger['tid'] for finger in converted['fingers']]
Joseph Hwang4782a042015-04-08 17:15:50 +0800499 for tid in set(self._prev_tids) - set(curr_tids):
500 leaving_finger = {'tid': tid, 'leaving': True}
Jingkui Wangcfabdd52017-06-27 10:28:20 -0700501 converted['fingers'].append(leaving_finger)
Joseph Hwang4782a042015-04-08 17:15:50 +0800502 self._prev_tids = curr_tids
503
Joseph Hwang02e829c2015-04-13 17:09:07 +0800504 # Convert raw events from a list of classes to a list of its strings
505 # so that the raw_events is serializable.
506 converted['raw_events'] = [str(event) for event in converted['raw_events']]
507
Joseph Hwang4782a042015-04-08 17:15:50 +0800508 return converted
509
510 def GetSnapshot(self):
511 """Get a snapshot from the touch device."""
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700512 return self._device.NextSnapshot()
Joseph Hwang4782a042015-04-08 17:15:50 +0800513
514 def AddSnapshot(self, snapshot):
515 """Convert the snapshot to a proper format and publish it to clients."""
Johny Lin908b92a2015-08-27 21:59:30 +0800516 if not self._is_centroiding:
517 snapshot = self._ConvertNamedtupleToDict(snapshot)
Joseph Hwang4782a042015-04-08 17:15:50 +0800518 cherrypy.engine.publish('websocket-broadcast', json.dumps(snapshot))
Joseph Hwang02e829c2015-04-13 17:09:07 +0800519 return snapshot
Joseph Hwang4782a042015-04-08 17:15:50 +0800520
521 def GetAndPlotSnapshots(self):
522 """Get and plot snapshots."""
523 cherrypy.log('Start getting the live stream snapshots....')
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800524 try:
525 with open(self._saved_file, 'w') as f:
526 while True:
527 try:
528 snapshot = self.GetSnapshot()
529 if not snapshot:
530 cherrypy.log('webplot is terminated.')
531 break
532 converted_snapshot = self.AddSnapshot(snapshot)
533 f.write('\n'.join(converted_snapshot['raw_events']) + '\n')
534 f.flush()
535 except KeyboardInterrupt:
536 cherrypy.log('Keyboard Interrupt accepted')
537 cherrypy.log('webplot is being terminated...')
538 state.QuitAndShutdown()
539 except IOError as e:
540 _IOError(e, self._saved_file)
541 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800542
543 def Publish(self, msg):
544 """Publish a message to clients."""
545 cherrypy.engine.publish('websocket-broadcast', TextMessage(msg))
546
547 def Clear(self):
548 """Notify clients to clear the display."""
549 self.Publish('clear')
550
551 def Quit(self):
552 """Notify clients to quit.
553
554 Note that the cherrypy engine would quit accordingly.
555 """
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800556 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800557
Charlie Mooneybf469942015-07-09 11:21:12 -0700558 def Save(self):
559 """Notify clients to save the screen, then wait for the image file to be
560 created, and return the image.
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700561 """
562 global image_lock
563 global image_string
564
565 # Trigger a save action
Joseph Hwang4782a042015-04-08 17:15:50 +0800566 self.Publish('save')
567
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700568 # Block until the server has completed saving it to disk
569 image_lock.wait()
570 image_lock.clear()
571 return image_string
572
Joseph Hwang4782a042015-04-08 17:15:50 +0800573 def Url(self):
574 """The url the server is serving at."""
575 return 'http://%s:%d' % (self._server_addr, self._server_port)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700576
577
Joseph Hwanga1c84782015-04-14 15:07:00 +0800578def _CheckLegalUser():
579 """If this program is run in chroot, it should not be run as root for security
580 reason.
581 """
582 if os.path.exists('/etc/cros_chroot_version') and os.getuid() == 0:
583 print ('You should run webplot in chroot as a regular user '
584 'instead of as root.\n')
585 exit(1)
586
587
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700588def _ParseArguments():
589 """Parse the command line options."""
590 parser = argparse.ArgumentParser(description='Webplot Server')
Charlie Mooney8026f2a2015-04-02 13:01:28 -0700591 parser.add_argument('-d', '--dut_addr', default=None,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700592 help='the address of the dut')
Joseph Hwang59db4412015-04-09 12:43:16 +0800593
594 # Make an exclusive group to make the webplot.py command option
595 # consistent with the webplot.sh script command option.
596 # What is desired:
597 # When no command option specified in webplot.sh/webplot.py: grab is True
598 # When '--grab' option specified in webplot.sh/webplot.py: grab is True
599 # When '--nograb' option specified in webplot.sh/webplot.py: grab is False
600 grab_group = parser.add_mutually_exclusive_group()
601 grab_group.add_argument('--grab', help='grab the device exclusively',
602 action='store_true')
603 grab_group.add_argument('--nograb', help='do not grab the device',
604 action='store_true')
605
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700606 parser.add_argument('--is_touchscreen', help='the DUT is touchscreen',
607 action='store_true')
Charlie Mooneye15a5552015-07-10 13:48:03 -0700608 parser.add_argument('-p', '--server_port', default=8080, type=int,
609 help='the port the web server listens to (default: 8080)')
610 parser.add_argument('--behind_firewall', action='store_true',
611 help=('With this flag set, you tell webplot to add a '
612 'rule to iptables to allow incoming traffic to '
613 'the webserver. If you are running webplot on '
614 'a chromebook, this is needed.'))
Jingkui Wangbafba932018-05-03 11:16:19 -0700615 parser.add_argument('-s', '--server_addr', default='0.0.0.0',
Joseph Hwang59db4412015-04-09 12:43:16 +0800616 help='the address the webplot http server listens to')
617 parser.add_argument('-t', '--dut_type', default='chromeos', type=str.lower,
Johny Lin908b92a2015-08-27 21:59:30 +0800618 help='dut type: chromeos, android, centroiding')
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700619 parser.add_argument('--automatically_start_browser', action='store_true',
620 help=('When this flag is set the script will try to '
621 'start a web browser automatically once webplot '
622 'is ready, instead of waiting for the user to.'))
Charlie Mooney60b56a72016-09-26 12:35:53 -0700623 parser.add_argument('--protocol',type=str, default='auto',
624 choices=['auto', 'stylus', 'MTB', 'MTA'],
625 help=('Which protocol does the device use? Choose from '
626 'auto, MTB, MTA, or stylus'))
Johny Lin908b92a2015-08-27 21:59:30 +0800627
628 # Arguments especial for centroiding visualizing tool.
629 # Please set "--dut_type centroiding" for centroiding utility.
630 parser.add_argument('-f', '--dut_forward_port', default=12345, type=int,
631 help='the forwarding port for centroiding socket server '
632 '(default: 12345) (only needed for centroiding)')
633 parser.add_argument('-c', '--config', default='tango.conf', type=str,
634 help='Config file name of device for centroiding '
635 'visualizing tool parameters.')
636 parser.add_argument('--fps', default=0, type=int,
637 help='the target frame rate of visualizer plotting, set '
638 '0 for keeping same as centroiding processing frame '
639 'rate.')
640
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700641 args = parser.parse_args()
Joseph Hwang59db4412015-04-09 12:43:16 +0800642
643 args.grab = not args.nograb
644
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700645 return args
646
647
648def Main():
649 """The main function to launch webplot service."""
Joseph Hwanga1c84782015-04-14 15:07:00 +0800650 _CheckLegalUser()
651
Joseph Hwange9cfd642015-04-20 16:00:33 +0800652 configure_logger(level=logging.ERROR)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700653 args = _ParseArguments()
654
Johny Lin04d970a2015-12-08 03:20:13 +0800655 # Specify Webplot for centroiding purpose.
656 is_centroiding = args.dut_type == 'centroiding'
657
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700658 print '\n' + '-' * 70
Johny Lin04d970a2015-12-08 03:20:13 +0800659 if is_centroiding:
Johny Lin908b92a2015-08-27 21:59:30 +0800660 cherrypy.log('**** Centroiding Data Visualizing Tool ****')
661 cherrypy.log('dut config file: %s' % args.config)
662 cherrypy.log('dut address: %s' % args.dut_addr)
663 cherrypy.log('dut socket forwarding port: %d' % args.dut_forward_port)
664 else:
665 cherrypy.log('dut machine type: %s' % args.dut_type)
666 cherrypy.log('dut\'s touch device: %s' %
667 ('touchscreen' if args.is_touchscreen else 'touchpad'))
668 cherrypy.log('dut address: %s' % args.dut_addr)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700669 cherrypy.log('web server address: %s' % args.server_addr)
670 cherrypy.log('web server port: %s' % args.server_port)
Joseph Hwang59db4412015-04-09 12:43:16 +0800671 cherrypy.log('grab the touch device: %s' % args.grab)
672 if args.dut_type == 'android' and args.grab:
673 cherrypy.log('Warning: the grab option is not supported on Android devices'
674 ' yet.')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700675 cherrypy.log('touch events are saved in %s' % SAVED_FILE)
676 print '-' * 70 + '\n\n'
677
678 if args.server_port == 80:
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700679 url = 'http://%s' % args.server_addr
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700680 else:
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700681 url = 'http://%s:%d' % (args.server_addr, args.server_port)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700682
683 msg = 'Type "%s" in browser %s to see finger traces.\n'
Charlie Mooneye15a5552015-07-10 13:48:03 -0700684 if args.server_addr == '127.0.0.1':
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700685 which_machine = 'on the webplot server machine'
686 else:
687 which_machine = 'on any machine'
688
689 print '*' * 70
690 print msg % (url, which_machine)
691 print 'Press \'q\' on the browser to quit.'
692 print '*' * 70 + '\n\n'
693
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700694 # Instantiate a touch device.
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700695 if args.dut_type == 'chromeos':
696 addr = args.dut_addr if args.dut_addr else '127.0.0.1'
Charlie Mooney60b56a72016-09-26 12:35:53 -0700697 device = ChromeOSTouchDevice(addr, args.is_touchscreen, grab=args.grab,
698 protocol=args.protocol)
Johny Lin908b92a2015-08-27 21:59:30 +0800699 elif args.dut_type == 'android':
Charlie Mooney60b56a72016-09-26 12:35:53 -0700700 device = AndroidTouchDevice(args.dut_addr, True, protocol=args.protocol)
Johny Lin04d970a2015-12-08 03:20:13 +0800701 elif is_centroiding: # args.dut_type == 'centroiding'
702 # Import centroiding library conditionally to avoid missing dependency.
703 from centroiding import CentroidingDataReceiver, CentroidingDevice
Johny Lin908b92a2015-08-27 21:59:30 +0800704 device = CentroidingDevice(args.config)
Johny Lin04d970a2015-12-08 03:20:13 +0800705 else:
706 print 'Unrecognized dut_type: %s. Webplot is aborted...' % args.dut_type
707 exit(1)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700708
Joseph Hwang4782a042015-04-08 17:15:50 +0800709 # Instantiate a webplot server daemon and start it.
Charlie Mooneye15a5552015-07-10 13:48:03 -0700710 webplot = Webplot(args.server_addr, args.server_port, device, logging=True,
Johny Lin908b92a2015-08-27 21:59:30 +0800711 is_behind_iptables_firewall=args.behind_firewall,
712 is_centroiding=is_centroiding)
Joseph Hwang4782a042015-04-08 17:15:50 +0800713 webplot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700714
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700715 if args.automatically_start_browser:
716 opened_successfully = webbrowser.open(url)
717 if opened_successfully:
718 print 'Web browser opened successfully!'
719 else:
720 print '!' * 80
721 print 'Sorry, we were unable to automatically open a web browser for you'
722 print 'Please navigate to "%s" in a browser manually, instead' % url
723 print '!' * 80
724
Johny Lin908b92a2015-08-27 21:59:30 +0800725 if not is_centroiding:
726 # Get touch snapshots from the touch device and have clients plot them.
727 webplot.GetAndPlotSnapshots()
728 else:
729 receiver = CentroidingDataReceiver(
730 '127.0.0.1', args.dut_forward_port, webplot, plot_fps=args.fps)
731 receiver.StartReceive()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700732
733
734if __name__ == '__main__':
735 Main()