blob: 2df21ff0a57fc66f07a0f527e111ac88435aa799 [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
Johny Lin908b92a2015-08-27 21:59:30 +080028from centroiding import CentroidingDataReceiver, CentroidingDevice
Charlie Mooney04b41532015-04-02 12:41:37 -070029from remote import ChromeOSTouchDevice, AndroidTouchDevice
Charlie Mooneybbc05f52015-03-24 13:36:22 -070030
31
32# The WebSocket connection state object.
33state = None
34
35# The touch events are saved in this file as default.
Charlie Mooneybf469942015-07-09 11:21:12 -070036current_username = pwd.getpwuid(os.getuid()).pw_name
37SAVED_FILE = '/tmp/webplot_%s.dat' % current_username
38SAVED_IMAGE = '/tmp/webplot_%s.png' % current_username
Charlie Mooneybbc05f52015-03-24 13:36:22 -070039
40
Joseph Hwang4782a042015-04-08 17:15:50 +080041def SimpleSystem(cmd):
42 """Execute a system command."""
43 ret = subprocess.call(cmd, shell=True)
44 if ret:
45 logging.warning('Command (%s) failed (ret=%s).', cmd, ret)
46 return ret
47
48
49def SimpleSystemOutput(cmd):
50 """Execute a system command and get its output."""
51 try:
52 proc = subprocess.Popen(
53 cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
54 stdout, _ = proc.communicate()
55 except Exception, e:
56 logging.warning('Command (%s) failed (%s).', cmd, e)
57 else:
58 return None if proc.returncode else stdout.strip()
59
60
61def IsDestinationPortEnabled(port):
62 """Check if the destination port is enabled in iptables.
63
64 If port 8000 is enabled, it looks like
65 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 ctstate NEW tcp dpt:8000
66 """
67 pattern = re.compile('ACCEPT\s+tcp.+\s+ctstate\s+NEW\s+tcp\s+dpt:%d' % port)
68 rules = SimpleSystemOutput('sudo iptables -L INPUT -n --line-number')
69 for rule in rules.splitlines():
70 if pattern.search(rule):
71 return True
72 return False
73
74
75def EnableDestinationPort(port):
76 """Enable the destination port for input traffic in iptables."""
77 if IsDestinationPortEnabled(port):
78 cherrypy.log('Port %d has been already enabled in iptables.' % port)
79 else:
Charlie Mooneye15a5552015-07-10 13:48:03 -070080 cherrypy.log('Adding a rule to accept incoming connections on port %d in '
81 'iptables.' % port)
Joseph Hwang4782a042015-04-08 17:15:50 +080082 cmd = ('sudo iptables -A INPUT -p tcp -m conntrack --ctstate NEW '
83 '--dport %d -j ACCEPT' % port)
84 if SimpleSystem(cmd) != 0:
85 raise Error('Failed to enable port in iptables: %d.' % port)
86
87
Charlie Mooneybbc05f52015-03-24 13:36:22 -070088def InterruptHandler():
89 """An interrupt handler for both SIGINT and SIGTERM
90
91 The stop procedure triggered is as follows:
92 1. This handler sends a 'quit' message to the listening client.
93 2. The client sends the canvas image back to the server in its quit message.
94 3. WebplotWSHandler.received_message() saves the image.
95 4. WebplotWSHandler.received_message() handles the 'quit' message.
96 The cherrypy engine exits if this is the last client.
97 """
98 cherrypy.log('Cherrypy engine is sending quit message to clients.')
Joseph Hwang95bf52a2015-04-14 13:09:39 +080099 state.QuitAndShutdown()
100
101
102def _IOError(e, filename):
103 err_msg = ['\n', '!' * 60, str(e),
104 'It is likely that %s is owned by root.' % filename,
105 'Please remove the file and then run webplot again.',
106 '!' * 60, '\n']
107 cherrypy.log('\n'.join(err_msg))
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700108
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700109image_lock = threading.Event()
110image_string = ''
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700111
112class WebplotWSHandler(WebSocket):
113 """The web socket handler for webplot."""
114
115 def opened(self):
116 """This method is called when the handler is opened."""
117 cherrypy.log('WS handler is opened!')
118
119 def received_message(self, msg):
120 """A callback for received message."""
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700121 data = msg.data.split(':', 1)
122 mtype = data[0].lower()
123 content = data[1] if len(data) == 2 else None
Joseph Hwang0c1fa7d2015-04-09 17:01:45 +0800124
125 # Do not print the image data since it is too large.
126 if mtype != 'save':
127 cherrypy.log('Received message: %s' % str(msg.data))
128
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700129 if mtype == 'quit':
130 # A shutdown message requested by the user.
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700131 cherrypy.log('The user requests to shutdown the cherrypy server....')
132 state.DecCount()
133 elif mtype == 'save':
Charlie Mooneyb476e892015-04-02 13:25:49 -0700134 cherrypy.log('All data saved to "%s"' % SAVED_FILE)
135 self.SaveImage(content, SAVED_IMAGE)
136 cherrypy.log('Plot image saved to "%s"' % SAVED_IMAGE)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700137 else:
138 cherrypy.log('Unknown message type: %s' % mtype)
139
140 def closed(self, code, reason="A client left the room."):
141 """This method is called when the handler is closed."""
142 cherrypy.log('A client requests to close WS.')
143 cherrypy.engine.publish('websocket-broadcast', TextMessage(reason))
144
145 @staticmethod
146 def SaveImage(image_data, image_file):
147 """Decoded the base64 image data and save it in the file."""
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700148 global image_string
149 image_string = base64.b64decode(image_data)
150 image_lock.set()
Joseph Hwangafc092c2015-04-21 11:32:45 +0800151 try:
152 with open(image_file, 'w') as f:
153 f.write(image_string)
154 except IOError as e:
155 _IOError(e, image_file)
156 state.QuitAndShutdown()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700157
158
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700159class ConnectionState(object):
160 """A ws connection state object for shutting down the cherrypy server.
161
162 It shuts down the cherrypy server when the count is down to 0 and is not
163 increased before the shutdown_timer expires.
164
165 Note that when a page refreshes, it closes the WS connection first and
166 then re-connects immediately. This is why we would like to wait a while
167 before actually shutting down the server.
168 """
169 TIMEOUT = 1.0
170
171 def __init__(self):
172 self.count = 0;
173 self.lock = threading.Lock()
174 self.shutdown_timer = None
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800175 self.quit_flag = False
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700176
177 def IncCount(self):
178 """Increase the connection count, and cancel the shutdown timer if exists.
179 """
180 self.lock.acquire()
181 self.count += 1;
182 cherrypy.log(' WS connection count: %d' % self.count)
183 if self.shutdown_timer:
184 self.shutdown_timer.cancel()
185 self.shutdown_timer = None
186 self.lock.release()
187
188 def DecCount(self):
189 """Decrease the connection count, and start a shutdown timer if no other
190 clients are connecting to the server.
191 """
192 self.lock.acquire()
193 self.count -= 1;
194 cherrypy.log(' WS connection count: %d' % self.count)
195 if self.count == 0:
196 self.shutdown_timer = threading.Timer(self.TIMEOUT, self.Shutdown)
197 self.shutdown_timer.start()
198 self.lock.release()
199
Joseph Hwang3f561d32015-04-09 16:33:56 +0800200 def ShutdownWhenNoConnections(self):
201 """Shutdown cherrypy server when there is no client connection."""
202 self.lock.acquire()
203 if self.count == 0 and self.shutdown_timer is None:
204 self.Shutdown()
205 self.lock.release()
206
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700207 def Shutdown(self):
208 """Shutdown the cherrypy server."""
209 cherrypy.log('Shutdown timer expires. Cherrypy server for Webplot exits.')
210 cherrypy.engine.exit()
211
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800212 def QuitAndShutdown(self):
213 """The server notifies clients to quit and then shuts down."""
214 if not self.quit_flag:
215 self.quit_flag = True
216 cherrypy.engine.publish('websocket-broadcast', TextMessage('quit'))
217 self.ShutdownWhenNoConnections()
218
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700219
Johny Lin908b92a2015-08-27 21:59:30 +0800220class TouchRoot(object):
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700221 """A class to handle requests about docroot."""
222
223 def __init__(self, ip, port, touch_min_x, touch_max_x, touch_min_y,
224 touch_max_y, touch_min_pressure, touch_max_pressure):
225 self.ip = ip
226 self.port = port
227 self.touch_min_x = touch_min_x
228 self.touch_max_x = touch_max_x
229 self.touch_min_y = touch_min_y
230 self.touch_max_y = touch_max_y
231 self.touch_min_pressure = touch_min_pressure
232 self.touch_max_pressure = touch_max_pressure
233 self.scheme = 'ws'
234 cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
235 cherrypy.log('scheme: %s' % self.scheme)
236
237 @cherrypy.expose
238 def index(self):
239 """This is the default index.html page."""
240 websocket_dict = {
241 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
242 'touchMinX': str(self.touch_min_x),
243 'touchMaxX': str(self.touch_max_x),
244 'touchMinY': str(self.touch_min_y),
245 'touchMaxY': str(self.touch_max_y),
246 'touchMinPressure': str(self.touch_min_pressure),
247 'touchMaxPressure': str(self.touch_max_pressure),
248 }
249 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
250 'webplot.html')
251 with open(root_page) as f:
252 return f.read() % websocket_dict
253
254 @cherrypy.expose
255 def ws(self):
256 """This handles the request to create a new web socket per client."""
257 cherrypy.log('A new client requesting for WS')
258 cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
259 state.IncCount()
260
261
Johny Lin908b92a2015-08-27 21:59:30 +0800262class CentroidingRoot(object):
263 """A class to handle requests about docroot."""
264
265 def __init__(self, ip, port, data_scale, data_offset,
266 data_width, data_height):
267 self.ip = ip
268 self.port = port
269 self.data_scale = data_scale
270 self.data_offset = data_offset
271 self.data_width = data_width
272 self.data_height = data_height
273 self.scheme = 'ws'
274 cherrypy.log('Root address: (%s, %s)' % (ip, str(port)))
275 cherrypy.log('scheme: %s' % self.scheme)
276
277 @cherrypy.expose
278 def index(self):
279 """This is the default index.html page."""
280 websocket_dict = {
281 'websocketUrl': '%s://%s:%s/ws' % (self.scheme, self.ip, self.port),
282 'dataScale': str(self.data_scale),
283 'dataOffset': str(self.data_offset),
284 'dataWidth': str(self.data_width),
285 'dataHeight': str(self.data_height),
286 }
287 print websocket_dict
288 root_page = os.path.join(os.path.abspath(os.path.dirname(__file__)),
289 'centroiding.html')
290 with open(root_page) as f:
291 return f.read() % websocket_dict
292
293 @cherrypy.expose
294 def ws(self):
295 """This handles the request to create a new web socket per client."""
296 cherrypy.log('A new client requesting for WS')
297 cherrypy.log('WS handler created: %s' % repr(cherrypy.request.ws_handler))
298 state.IncCount()
299
300
Joseph Hwang4782a042015-04-08 17:15:50 +0800301class Webplot(threading.Thread):
302 """The server handling the Plotting of finger traces.
303
304 Use case 1: embedding Webplot as a plotter in an application
305
306 # Instantiate a webplot server and starts the daemon.
307 plot = Webplot(server_addr, server_port, device)
308 plot.start()
309
310 # Repeatedly get a snapshot and add it for plotting.
311 while True:
312 # GetSnapshot() is essentially device.NextSnapshot()
313 snapshot = plot.GetSnapshot()
314 if not snapshot:
315 break
316 # Add the snapshot to the plotter for plotting.
317 plot.AddSnapshot(snapshot)
318
319 # Save a screen dump
320 plot.Save()
321
322 # Notify the browser to clear the screen.
323 plot.Clear()
324
325 # Notify both the browser and the cherrypy engine to quit.
326 plot.Quit()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700327
328
Joseph Hwang4782a042015-04-08 17:15:50 +0800329 Use case 2: using webplot standalone
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700330
Joseph Hwang4782a042015-04-08 17:15:50 +0800331 # Instantiate a webplot server and starts the daemon.
332 plot = Webplot(server_addr, server_port, device)
333 plot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700334
Joseph Hwang4782a042015-04-08 17:15:50 +0800335 # Get touch snapshots from the touch device and have clients plot them.
336 webplot.GetAndPlotSnapshots()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700337 """
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700338
Joseph Hwange9cfd642015-04-20 16:00:33 +0800339 def __init__(self, server_addr, server_port, device, saved_file=SAVED_FILE,
Johny Lin908b92a2015-08-27 21:59:30 +0800340 logging=False, is_behind_iptables_firewall=False,
341 is_centroiding=False):
Joseph Hwang4782a042015-04-08 17:15:50 +0800342 self._server_addr = server_addr
343 self._server_port = server_port
344 self._device = device
345 self._saved_file = saved_file
Johny Lin908b92a2015-08-27 21:59:30 +0800346 self._is_centroiding = is_centroiding
Joseph Hwang4782a042015-04-08 17:15:50 +0800347 super(Webplot, self).__init__(name='webplot thread')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700348
Joseph Hwang4782a042015-04-08 17:15:50 +0800349 self.daemon = True
350 self._prev_tids = []
351
Joseph Hwange9cfd642015-04-20 16:00:33 +0800352 # The logging is turned off by default when imported as a module so that
353 # it does not mess up the screen.
354 if not logging:
355 cherrypy.log.screen = None
356
Charlie Mooneye15a5552015-07-10 13:48:03 -0700357 # Allow input traffic in iptables, if the user has specified. This setting
358 # should be used if webplot is being run directly on a chromebook, but it
359 # requires root access, so we don't want to use it all the time.
360 if is_behind_iptables_firewall:
361 EnableDestinationPort(self._server_port)
Joseph Hwang4782a042015-04-08 17:15:50 +0800362
363 # Create a ws connection state object to wait for the condition to
364 # shutdown the whole process.
365 global state
366 state = ConnectionState()
367
368 cherrypy.config.update({
369 'server.socket_host': self._server_addr,
370 'server.socket_port': self._server_port,
371 })
372
373 WebSocketPlugin(cherrypy.engine).subscribe()
374 cherrypy.tools.websocket = WebSocketTool()
375
376 # If the cherrypy server exits for whatever reason, close the device
377 # for required cleanup. Otherwise, there might exist local/remote
378 # zombie processes.
Johny Lin908b92a2015-08-27 21:59:30 +0800379 if not self._is_centroiding:
380 cherrypy.engine.subscribe('exit', self._device.__del__)
Joseph Hwang4782a042015-04-08 17:15:50 +0800381
382 cherrypy.engine.signal_handler.handlers['SIGINT'] = InterruptHandler
383 cherrypy.engine.signal_handler.handlers['SIGTERM'] = InterruptHandler
384
385 def run(self):
386 """Start the cherrypy engine."""
Johny Lin908b92a2015-08-27 21:59:30 +0800387 if not self._is_centroiding:
388 x_min, x_max = self._device.RangeX()
389 y_min, y_max = self._device.RangeY()
390 p_min, p_max = self._device.RangeP()
391 root = TouchRoot(self._server_addr, self._server_port,
392 x_min, x_max, y_min, y_max, p_min, p_max)
393 else:
394 data_scale = self._device.data_scale
395 data_offset = self._device.data_offset
396 data_width = self._device.width
397 data_height = self._device.height
398 root = CentroidingRoot(self._server_addr, self._server_port,
399 data_scale, data_offset, data_width, data_height)
Joseph Hwang4782a042015-04-08 17:15:50 +0800400
401 cherrypy.quickstart(
Johny Lin908b92a2015-08-27 21:59:30 +0800402 root,
Joseph Hwang4782a042015-04-08 17:15:50 +0800403 '',
404 config={
405 '/': {
406 'tools.staticdir.root':
407 os.path.abspath(os.path.dirname(__file__)),
408 'tools.staticdir.on': True,
409 'tools.staticdir.dir': '',
410 },
411 '/ws': {
412 'tools.websocket.on': True,
413 'tools.websocket.handler_cls': WebplotWSHandler,
414 },
415 }
416 )
417
418 def _ConvertNamedtupleToDict(self, snapshot):
419 """Convert namedtuples to ordinary dictionaries and add leaving slots.
420
421 This is to make a snapshot json serializable. Otherwise, the namedtuples
422 would be transmitted as arrays which is less readable.
423
424 A snapshot looks like
425 MtSnapshot(
426 syn_time=1420524008.368854,
427 button_pressed=False,
428 fingers=[
429 MtFinger(tid=162, slot=0, syn_time=1420524008.368854, x=524,
430 y=231, pressure=45),
431 MtFinger(tid=163, slot=1, syn_time=1420524008.368854, x=677,
432 y=135, pressure=57)
433 ]
434 )
435
436 Note:
437 1. that there are two levels of namedtuples to convert.
438 2. The leaving slots are used to notify javascript that a finger is leaving
439 so that the corresponding finger color could be released for reuse.
440 """
441 # Convert MtSnapshot.
442 converted = dict(snapshot.__dict__.items())
443
444 # Convert MtFinger.
445 converted['fingers'] = [dict(finger.__dict__.items())
446 for finger in converted['fingers']]
447 converted['raw_events'] = [str(event) for event in converted['raw_events']]
448
449 # Add leaving fingers to notify js for reclaiming the finger colors.
450 curr_tids = [finger['tid'] for finger in converted['fingers']]
451 for tid in set(self._prev_tids) - set(curr_tids):
452 leaving_finger = {'tid': tid, 'leaving': True}
453 converted['fingers'].append(leaving_finger)
454 self._prev_tids = curr_tids
455
Joseph Hwang02e829c2015-04-13 17:09:07 +0800456 # Convert raw events from a list of classes to a list of its strings
457 # so that the raw_events is serializable.
458 converted['raw_events'] = [str(event) for event in converted['raw_events']]
459
Joseph Hwang4782a042015-04-08 17:15:50 +0800460 return converted
461
462 def GetSnapshot(self):
463 """Get a snapshot from the touch device."""
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700464 return self._device.NextSnapshot()
Joseph Hwang4782a042015-04-08 17:15:50 +0800465
466 def AddSnapshot(self, snapshot):
467 """Convert the snapshot to a proper format and publish it to clients."""
Johny Lin908b92a2015-08-27 21:59:30 +0800468 if not self._is_centroiding:
469 snapshot = self._ConvertNamedtupleToDict(snapshot)
Joseph Hwang4782a042015-04-08 17:15:50 +0800470 cherrypy.engine.publish('websocket-broadcast', json.dumps(snapshot))
Joseph Hwang02e829c2015-04-13 17:09:07 +0800471 return snapshot
Joseph Hwang4782a042015-04-08 17:15:50 +0800472
473 def GetAndPlotSnapshots(self):
474 """Get and plot snapshots."""
475 cherrypy.log('Start getting the live stream snapshots....')
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800476 try:
477 with open(self._saved_file, 'w') as f:
478 while True:
479 try:
480 snapshot = self.GetSnapshot()
481 if not snapshot:
482 cherrypy.log('webplot is terminated.')
483 break
484 converted_snapshot = self.AddSnapshot(snapshot)
485 f.write('\n'.join(converted_snapshot['raw_events']) + '\n')
486 f.flush()
487 except KeyboardInterrupt:
488 cherrypy.log('Keyboard Interrupt accepted')
489 cherrypy.log('webplot is being terminated...')
490 state.QuitAndShutdown()
491 except IOError as e:
492 _IOError(e, self._saved_file)
493 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800494
495 def Publish(self, msg):
496 """Publish a message to clients."""
497 cherrypy.engine.publish('websocket-broadcast', TextMessage(msg))
498
499 def Clear(self):
500 """Notify clients to clear the display."""
501 self.Publish('clear')
502
503 def Quit(self):
504 """Notify clients to quit.
505
506 Note that the cherrypy engine would quit accordingly.
507 """
Joseph Hwang95bf52a2015-04-14 13:09:39 +0800508 state.QuitAndShutdown()
Joseph Hwang4782a042015-04-08 17:15:50 +0800509
Charlie Mooneybf469942015-07-09 11:21:12 -0700510 def Save(self):
511 """Notify clients to save the screen, then wait for the image file to be
512 created, and return the image.
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700513 """
514 global image_lock
515 global image_string
516
517 # Trigger a save action
Joseph Hwang4782a042015-04-08 17:15:50 +0800518 self.Publish('save')
519
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700520 # Block until the server has completed saving it to disk
521 image_lock.wait()
522 image_lock.clear()
523 return image_string
524
Joseph Hwang4782a042015-04-08 17:15:50 +0800525 def Url(self):
526 """The url the server is serving at."""
527 return 'http://%s:%d' % (self._server_addr, self._server_port)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700528
529
Joseph Hwanga1c84782015-04-14 15:07:00 +0800530def _CheckLegalUser():
531 """If this program is run in chroot, it should not be run as root for security
532 reason.
533 """
534 if os.path.exists('/etc/cros_chroot_version') and os.getuid() == 0:
535 print ('You should run webplot in chroot as a regular user '
536 'instead of as root.\n')
537 exit(1)
538
539
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700540def _ParseArguments():
541 """Parse the command line options."""
542 parser = argparse.ArgumentParser(description='Webplot Server')
Charlie Mooney8026f2a2015-04-02 13:01:28 -0700543 parser.add_argument('-d', '--dut_addr', default=None,
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700544 help='the address of the dut')
Joseph Hwang59db4412015-04-09 12:43:16 +0800545
546 # Make an exclusive group to make the webplot.py command option
547 # consistent with the webplot.sh script command option.
548 # What is desired:
549 # When no command option specified in webplot.sh/webplot.py: grab is True
550 # When '--grab' option specified in webplot.sh/webplot.py: grab is True
551 # When '--nograb' option specified in webplot.sh/webplot.py: grab is False
552 grab_group = parser.add_mutually_exclusive_group()
553 grab_group.add_argument('--grab', help='grab the device exclusively',
554 action='store_true')
555 grab_group.add_argument('--nograb', help='do not grab the device',
556 action='store_true')
557
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700558 parser.add_argument('--is_touchscreen', help='the DUT is touchscreen',
559 action='store_true')
Charlie Mooneye15a5552015-07-10 13:48:03 -0700560 parser.add_argument('-p', '--server_port', default=8080, type=int,
561 help='the port the web server listens to (default: 8080)')
562 parser.add_argument('--behind_firewall', action='store_true',
563 help=('With this flag set, you tell webplot to add a '
564 'rule to iptables to allow incoming traffic to '
565 'the webserver. If you are running webplot on '
566 'a chromebook, this is needed.'))
567 parser.add_argument('-s', '--server_addr', default='127.0.0.1',
Joseph Hwang59db4412015-04-09 12:43:16 +0800568 help='the address the webplot http server listens to')
569 parser.add_argument('-t', '--dut_type', default='chromeos', type=str.lower,
Johny Lin908b92a2015-08-27 21:59:30 +0800570 help='dut type: chromeos, android, centroiding')
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700571 parser.add_argument('--automatically_start_browser', action='store_true',
572 help=('When this flag is set the script will try to '
573 'start a web browser automatically once webplot '
574 'is ready, instead of waiting for the user to.'))
Johny Lin908b92a2015-08-27 21:59:30 +0800575
576 # Arguments especial for centroiding visualizing tool.
577 # Please set "--dut_type centroiding" for centroiding utility.
578 parser.add_argument('-f', '--dut_forward_port', default=12345, type=int,
579 help='the forwarding port for centroiding socket server '
580 '(default: 12345) (only needed for centroiding)')
581 parser.add_argument('-c', '--config', default='tango.conf', type=str,
582 help='Config file name of device for centroiding '
583 'visualizing tool parameters.')
584 parser.add_argument('--fps', default=0, type=int,
585 help='the target frame rate of visualizer plotting, set '
586 '0 for keeping same as centroiding processing frame '
587 'rate.')
588
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700589 args = parser.parse_args()
Joseph Hwang59db4412015-04-09 12:43:16 +0800590
591 args.grab = not args.nograb
592
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700593 return args
594
595
596def Main():
597 """The main function to launch webplot service."""
Joseph Hwanga1c84782015-04-14 15:07:00 +0800598 _CheckLegalUser()
599
Joseph Hwange9cfd642015-04-20 16:00:33 +0800600 configure_logger(level=logging.ERROR)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700601 args = _ParseArguments()
602
603 print '\n' + '-' * 70
Johny Lin908b92a2015-08-27 21:59:30 +0800604 if args.dut_type == 'centroiding':
605 cherrypy.log('**** Centroiding Data Visualizing Tool ****')
606 cherrypy.log('dut config file: %s' % args.config)
607 cherrypy.log('dut address: %s' % args.dut_addr)
608 cherrypy.log('dut socket forwarding port: %d' % args.dut_forward_port)
609 else:
610 cherrypy.log('dut machine type: %s' % args.dut_type)
611 cherrypy.log('dut\'s touch device: %s' %
612 ('touchscreen' if args.is_touchscreen else 'touchpad'))
613 cherrypy.log('dut address: %s' % args.dut_addr)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700614 cherrypy.log('web server address: %s' % args.server_addr)
615 cherrypy.log('web server port: %s' % args.server_port)
Joseph Hwang59db4412015-04-09 12:43:16 +0800616 cherrypy.log('grab the touch device: %s' % args.grab)
617 if args.dut_type == 'android' and args.grab:
618 cherrypy.log('Warning: the grab option is not supported on Android devices'
619 ' yet.')
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700620 cherrypy.log('touch events are saved in %s' % SAVED_FILE)
621 print '-' * 70 + '\n\n'
622
623 if args.server_port == 80:
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700624 url = 'http://%s' % args.server_addr
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700625 else:
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700626 url = 'http://%s:%d' % (args.server_addr, args.server_port)
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700627
628 msg = 'Type "%s" in browser %s to see finger traces.\n'
Charlie Mooneye15a5552015-07-10 13:48:03 -0700629 if args.server_addr == '127.0.0.1':
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700630 which_machine = 'on the webplot server machine'
631 else:
632 which_machine = 'on any machine'
633
634 print '*' * 70
635 print msg % (url, which_machine)
636 print 'Press \'q\' on the browser to quit.'
637 print '*' * 70 + '\n\n'
638
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700639 # Instantiate a touch device.
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700640 if args.dut_type == 'chromeos':
641 addr = args.dut_addr if args.dut_addr else '127.0.0.1'
642 device = ChromeOSTouchDevice(addr, args.is_touchscreen, grab=args.grab)
Johny Lin908b92a2015-08-27 21:59:30 +0800643 elif args.dut_type == 'android':
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700644 device = AndroidTouchDevice(args.dut_addr, True)
Johny Lin908b92a2015-08-27 21:59:30 +0800645 else: # args.dut_type == 'centroiding'
646 device = CentroidingDevice(args.config)
Charlie Mooneyc68f9c32015-04-16 15:23:22 -0700647
Johny Lin908b92a2015-08-27 21:59:30 +0800648 # Specify Webplot for centroiding purpose.
649 is_centroiding = args.dut_type == 'centroiding'
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700650
Joseph Hwang4782a042015-04-08 17:15:50 +0800651 # Instantiate a webplot server daemon and start it.
Charlie Mooneye15a5552015-07-10 13:48:03 -0700652 webplot = Webplot(args.server_addr, args.server_port, device, logging=True,
Johny Lin908b92a2015-08-27 21:59:30 +0800653 is_behind_iptables_firewall=args.behind_firewall,
654 is_centroiding=is_centroiding)
Joseph Hwang4782a042015-04-08 17:15:50 +0800655 webplot.start()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700656
Charlie Mooney54e2f2e2015-07-09 10:53:38 -0700657 if args.automatically_start_browser:
658 opened_successfully = webbrowser.open(url)
659 if opened_successfully:
660 print 'Web browser opened successfully!'
661 else:
662 print '!' * 80
663 print 'Sorry, we were unable to automatically open a web browser for you'
664 print 'Please navigate to "%s" in a browser manually, instead' % url
665 print '!' * 80
666
Johny Lin908b92a2015-08-27 21:59:30 +0800667 if not is_centroiding:
668 # Get touch snapshots from the touch device and have clients plot them.
669 webplot.GetAndPlotSnapshots()
670 else:
671 receiver = CentroidingDataReceiver(
672 '127.0.0.1', args.dut_forward_port, webplot, plot_fps=args.fps)
673 receiver.StartReceive()
Charlie Mooneybbc05f52015-03-24 13:36:22 -0700674
675
676if __name__ == '__main__':
677 Main()