blob: f0b23905c9abee023a160582f07ad014022a995f [file] [log] [blame]
Simran Basia9f41032012-05-11 14:21:58 -07001# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Todd Broche505b8d2011-03-21 18:19:54 -07002# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Servo Server."""
Simran Basia9f41032012-05-11 14:21:58 -07005import fnmatch
Todd Broche505b8d2011-03-21 18:19:54 -07006import imp
7import logging
Simran Basia9f41032012-05-11 14:21:58 -07008import os
9import shutil
Todd Broche505b8d2011-03-21 18:19:54 -070010import SimpleXMLRPCServer
Simran Basia9f41032012-05-11 14:21:58 -070011import subprocess
12import tempfile
Todd Broch7a91c252012-02-03 12:37:45 -080013import time
Simran Basia9f41032012-05-11 14:21:58 -070014import urllib
Todd Broche505b8d2011-03-21 18:19:54 -070015
16# TODO(tbroch) deprecate use of relative imports
Vic Yangbe6cf262012-09-10 10:40:56 +080017from drv.hw_driver import HwDriverError
Todd Broche505b8d2011-03-21 18:19:54 -070018import ftdigpio
19import ftdii2c
Todd Brochdbb09982011-10-02 07:14:26 -070020import ftdi_common
Todd Broch47c43f42011-05-26 15:11:31 -070021import ftdiuart
Todd Broche505b8d2011-03-21 18:19:54 -070022
23MAX_I2C_CLOCK_HZ = 100000
24
Todd Brochdbb09982011-10-02 07:14:26 -070025
Todd Broche505b8d2011-03-21 18:19:54 -070026class ServodError(Exception):
27 """Exception class for servod."""
28
29class Servod(object):
30 """Main class for Servo debug/controller Daemon."""
Simran Basia9f41032012-05-11 14:21:58 -070031 _USB_DETECTION_DELAY = 10
32 _HTTP_PREFIX = "http://"
33
Todd Brochdbb09982011-10-02 07:14:26 -070034 def __init__(self, config, vendor, product, serialname=None, interfaces=None):
Todd Broche505b8d2011-03-21 18:19:54 -070035 """Servod constructor.
36
37 Args:
38 config: instance of SystemConfig containing all controls for
39 particular Servod invocation
40 vendor: usb vendor id of FTDI device
41 product: usb product id of FTDI device
Todd Brochad034442011-05-25 15:05:29 -070042 serialname: string of device serialname/number as defined in FTDI eeprom.
Todd Brochdbb09982011-10-02 07:14:26 -070043 interfaces: list of strings of interface types the server will instantiate
44
45 Raises:
46 ServodError: if unable to locate init method for particular interface
Todd Broche505b8d2011-03-21 18:19:54 -070047 """
48 self._logger = logging.getLogger("Servod")
49 self._logger.debug("")
50 self._vendor = vendor
51 self._product = product
Todd Brochad034442011-05-25 15:05:29 -070052 self._serialname = serialname
Todd Broche505b8d2011-03-21 18:19:54 -070053 self._syscfg = config
54 # list of objects (Fi2c, Fgpio) to physical interfaces (gpio, i2c) that ftdi
55 # interfaces are mapped to
56 self._interface_list = []
57 # Dict of Dict to map control name, function name to to tuple (params, drv)
58 # Ex) _drv_dict[name]['get'] = (params, drv)
59 self._drv_dict = {}
60
Todd Brochdbb09982011-10-02 07:14:26 -070061 # Note, interface i is (i - 1) in list
62 if not interfaces:
63 interfaces = ftdi_common.INTERFACE_DEFAULTS[vendor][product]
64
65 for i, name in enumerate(interfaces):
Todd Broch8a77a992012-01-27 09:46:08 -080066 # servos with multiple FTDI are guaranteed to have contiguous USB PIDs
67 if i and ((i % ftdi_common.MAX_FTDI_INTERFACES_PER_DEVICE) == 0):
68 self._product += 1
69 self._logger.info("Changing to next FTDI part @ pid = 0x%04x",
70 self._product)
71
Todd Brochdbb09982011-10-02 07:14:26 -070072 self._logger.info("Initializing FTDI interface %d to %s", i + 1, name)
73 try:
74 func = getattr(self, '_init_%s' % name)
75 except AttributeError:
76 raise ServodError("Unable to locate init for interface %s" % name)
Todd Brocha9c74692012-01-24 22:54:22 -080077 result = func((i % ftdi_common.MAX_FTDI_INTERFACES_PER_DEVICE) + 1)
Todd Broch888da782011-10-07 14:29:09 -070078 if isinstance(result, tuple):
79 self._interface_list.extend(result)
80 else:
81 self._interface_list.append(result)
Todd Broche505b8d2011-03-21 18:19:54 -070082
Todd Brochb3048492012-01-15 21:52:41 -080083 def _init_dummy(self, interface):
84 """Initialize dummy interface.
85
86 Dummy interface is just a mechanism to reserve that interface for non servod
87 interaction. Typically the interface will be managed by external
88 third-party tools like openOCD or urjtag for JTAG or flashrom for SPI
89 interfaces.
90
91 TODO(tbroch): Investigate merits of incorporating these third-party
92 interfaces into servod or creating a communication channel between them
93
94 Returns: None
95 """
96 return None
97
Todd Broche505b8d2011-03-21 18:19:54 -070098 def _init_gpio(self, interface):
99 """Initialize gpio driver interface and open for use.
100
101 Args:
102 interface: interface number of FTDI device to use.
103
104 Returns:
105 Instance object of interface.
Todd Broch6de9dc62012-04-09 15:23:53 -0700106
107 Raises:
108 ServodError: If init fails
Todd Broche505b8d2011-03-21 18:19:54 -0700109 """
Todd Brochad034442011-05-25 15:05:29 -0700110 fobj = ftdigpio.Fgpio(self._vendor, self._product, interface,
111 self._serialname)
Todd Broch6de9dc62012-04-09 15:23:53 -0700112 try:
113 fobj.open()
114 except ftdigpio.FgpioError as e:
115 raise ServodError('Opening gpio interface. %s ( %d )' % (e.msg, e.value))
116
Todd Broche505b8d2011-03-21 18:19:54 -0700117 return fobj
118
119 def _init_i2c(self, interface):
120 """Initialize i2c interface and open for use.
121
122 Args:
123 interface: interface number of FTDI device to use
124
125 Returns:
126 Instance object of interface
Todd Broch6de9dc62012-04-09 15:23:53 -0700127
128 Raises:
129 ServodError: If init fails
Todd Broche505b8d2011-03-21 18:19:54 -0700130 """
Todd Brochad034442011-05-25 15:05:29 -0700131 fobj = ftdii2c.Fi2c(self._vendor, self._product, interface,
132 self._serialname)
Todd Broch6de9dc62012-04-09 15:23:53 -0700133 try:
134 fobj.open()
135 except ftdii2c.Fi2cError as e:
136 raise ServodError('Opening i2c interface. %s ( %d )' % (e.msg, e.value))
137
Todd Broche505b8d2011-03-21 18:19:54 -0700138 # Set the frequency of operation of the i2c bus.
139 # TODO(tbroch) make configureable
140 fobj.setclock(MAX_I2C_CLOCK_HZ)
Todd Broch6de9dc62012-04-09 15:23:53 -0700141
Todd Broche505b8d2011-03-21 18:19:54 -0700142 return fobj
143
Todd Broch47c43f42011-05-26 15:11:31 -0700144 def _init_uart(self, interface):
145 """Initialize uart inteface and open for use
146
147 Note, the uart runs in a separate thread (pthreads). Users wishing to
148 interact with it will query control for the pty's pathname and connect
149 with there favorite console program. For example:
150 cu -l /dev/pts/22
151
152 Args:
153 interface: interface number of FTDI device to use
154
155 Returns:
156 Instance object of interface
Todd Broch6de9dc62012-04-09 15:23:53 -0700157
158 Raises:
159 ServodError: If init fails
Todd Broch47c43f42011-05-26 15:11:31 -0700160 """
Jeremy Thorpe9e110062012-10-25 10:40:00 -0700161 fobj = ftdiuart.Fuart(self._vendor, self._product, interface,
162 self._serialname)
Todd Broch6de9dc62012-04-09 15:23:53 -0700163 try:
164 fobj.run()
165 except ftdiuart.FuartError as e:
166 raise ServodError('Running uart interface. %s ( %d )' % (e.msg, e.value))
167
Todd Broch47c43f42011-05-26 15:11:31 -0700168 self._logger.info("%s" % fobj.get_pty())
169 return fobj
170
Todd Broch888da782011-10-07 14:29:09 -0700171 def _init_gpiouart(self, interface):
172 """Initialize special gpio + uart interface and open for use
173
174 Note, the uart runs in a separate thread (pthreads). Users wishing to
175 interact with it will query control for the pty's pathname and connect
176 with there favorite console program. For example:
177 cu -l /dev/pts/22
178
179 Args:
180 interface: interface number of FTDI device to use
181
182 Returns:
183 Instance objects of interface
Todd Broch6de9dc62012-04-09 15:23:53 -0700184
185 Raises:
186 ServodError: If init fails
Todd Broch888da782011-10-07 14:29:09 -0700187 """
188 fgpio = self._init_gpio(interface)
Jeremy Thorpe9e110062012-10-25 10:40:00 -0700189 fuart = ftdiuart.Fuart(self._vendor, self._product, interface,
190 self._serialname, fgpio._fc)
Todd Broch6de9dc62012-04-09 15:23:53 -0700191 try:
192 fuart.run()
193 except ftdiuart.FuartError as e:
194 raise ServodError('Running uart interface. %s ( %d )' % (e.msg, e.value))
195
Todd Broch888da782011-10-07 14:29:09 -0700196 self._logger.info("uart pty: %s" % fuart.get_pty())
197 return fgpio, fuart
198
Tom Wai-Hong Tam28f0a5f2012-08-21 12:49:57 +0800199 def _camel_case(self, string):
200 output = ''
201 for s in string.split('_'):
202 if output:
203 output += s.capitalize()
204 else:
205 output = s
206 return output
207
Todd Broche505b8d2011-03-21 18:19:54 -0700208 def _get_param_drv(self, control_name, is_get=True):
209 """Get access to driver for a given control.
210
211 Note, some controls have different parameter dictionaries for 'getting' the
212 control's value versus 'setting' it. Boolean is_get distinguishes which is
213 being requested.
214
215 Args:
216 control_name: string name of control
217 is_get: boolean to determine
218
219 Returns:
220 tuple (param, drv) where:
221 param: param dictionary for control
222 drv: instance object of driver for particular control
223
224 Raises:
225 ServodError: Error occurred while examining params dict
226 """
227 self._logger.debug("")
228 # if already setup just return tuple from driver dict
229 if control_name in self._drv_dict:
230 if is_get and ('get' in self._drv_dict[control_name]):
231 return self._drv_dict[control_name]['get']
232 if not is_get and ('set' in self._drv_dict[control_name]):
233 return self._drv_dict[control_name]['set']
234
235 params = self._syscfg.lookup_control_params(control_name, is_get)
236 if 'drv' not in params:
237 self._logger.error("Unable to determine driver for %s" % control_name)
238 raise ServodError("'drv' key not found in params dict")
239 if 'interface' not in params:
240 self._logger.error("Unable to determine interface for %s" %
241 control_name)
242
243 raise ServodError("'interface' key not found in params dict")
244 index = int(params['interface']) - 1
245 interface = self._interface_list[index]
246 servo_pkg = imp.load_module('servo', *imp.find_module('servo'))
247 drv_pkg = imp.load_module('drv',
248 *imp.find_module('drv', servo_pkg.__path__))
249 drv_name = params['drv']
250 drv_module = getattr(drv_pkg, drv_name)
Tom Wai-Hong Tam28f0a5f2012-08-21 12:49:57 +0800251 drv_class = getattr(drv_module, self._camel_case(drv_name))
Todd Broche505b8d2011-03-21 18:19:54 -0700252 drv = drv_class(interface, params)
253 if control_name not in self._drv_dict:
254 self._drv_dict[control_name] = {}
255 if is_get:
256 self._drv_dict[control_name]['get'] = (params, drv)
257 else:
258 self._drv_dict[control_name]['set'] = (params, drv)
259 return (params, drv)
260
261 def doc_all(self):
262 """Return all documenation for controls.
263
264 Returns:
265 string of <doc> text in config file (xml) and the params dictionary for
266 all controls.
267
268 For example:
269 warm_reset :: Reset the device warmly
270 ------------------------> {'interface': '1', 'map': 'onoff_i', ... }
271 """
272 return self._syscfg.display_config()
273
274 def doc(self, name):
275 """Retreive doc string in system config file for given control name.
276
277 Args:
278 name: name string of control to get doc string
279
280 Returns:
281 doc string of name
282
283 Raises:
284 NameError: if fails to locate control
285 """
286 self._logger.debug("name(%s)" % (name))
287 if self._syscfg.is_control(name):
288 return self._syscfg.get_control_docstring(name)
289 else:
290 raise NameError("No control %s" %name)
291
Simran Basia9f41032012-05-11 14:21:58 -0700292 def _get_usb_port_set(self):
293 """Gets a set of USB disks currently connected to the system
294
295 Returns:
296 A set of USB disk paths.
297 """
298 usb_set = fnmatch.filter(os.listdir("/dev/"), "sd[a-z]")
299 return set(["/dev/" + dev for dev in usb_set])
300
301 def _probe_host_usb_dev(self):
302 """Probe the USB disk device plugged in the servo from the host side.
303
304 Method can fail by:
305 1) Having multiple servos connected and returning incorrect /dev/sdX of
306 another servo.
307 2) Finding multiple /dev/sdX and returning None.
308
309 Returns:
310 USB disk path if one and only one USB disk path is found, otherwise None.
311 """
312 original_value = self.get("usb_mux_sel1")
313 # Make the host unable to see the USB disk.
314 if original_value != "dut_sees_usbkey":
315 self.set("usb_mux_sel1", "dut_sees_usbkey")
316 time.sleep(self._USB_DETECTION_DELAY)
317
318 no_usb_set = self._get_usb_port_set()
319 # Make the host able to see the USB disk.
320 self.set("usb_mux_sel1", "servo_sees_usbkey")
321 time.sleep(self._USB_DETECTION_DELAY)
322
323 has_usb_set = self._get_usb_port_set()
324 # Back to its original value.
325 if original_value != "servo_sees_usbkey":
326 self.set("usb_mux_sel1", original_value)
327 time.sleep(self._USB_DETECTION_DELAY)
328 # Subtract the two sets to find the usb device.
329 diff_set = has_usb_set - no_usb_set
330 if len(diff_set) == 1:
331 return diff_set.pop()
332 else:
333 return None
334
335 def download_image_to_usb(self, image_path):
336 """Download image and save to the USB device found by probe_host_usb_dev.
337 If the image_path is a URL, it will download this url to the USB path;
338 otherwise it will simply copy the image_path's contents to the USB path.
339
340 Args:
341 image_path: path or url to the recovery image.
342
343 Returns:
344 True|False: True if process completed successfully, False if error
345 occurred.
346 Can't return None because XMLRPC doesn't allow it. PTAL at tbroch's
347 comment at the end of set().
348 """
349 self._logger.debug("image_path(%s)" % image_path)
350 self._logger.debug("Detecting USB stick device...")
351 usb_dev = self._probe_host_usb_dev()
352 if not usb_dev:
353 self._logger.error("No usb device connected to servo")
354 return False
355
356 try:
357 if image_path.startswith(self._HTTP_PREFIX):
358 self._logger.debug("Image path is a URL, downloading image")
359 urllib.urlretrieve(image_path, usb_dev)
360 else:
361 shutil.copyfile(image_path, usb_dev)
362 except IOError as e:
363 self._logger.error("Failed to transfer image to USB device: %s ( %d ) ",
364 e.strerror, e.errno)
365 return False
366 except urllib.ContentTooShortError:
367 self._logger.error("Failed to download URL: %s to USB device: %s",
368 image_path, usb_dev)
369 return False
370 except BaseException as e:
371 self._logger.error("Unexpected exception downloading %s to %s: %s",
372 image_path, usb_dev, str(e))
373 return False
374 return True
375
376 def make_image_noninteractive(self):
377 """Makes the recovery image noninteractive.
378
379 A noninteractive image will reboot automatically after installation
380 instead of waiting for the USB device to be removed to initiate a system
381 reboot.
382
383 Mounts partition 1 of the image stored on usb_dev and creates a file
384 called "non_interactive" so that the image will become noninteractive.
385
386 Returns:
387 True|False: True if process completed successfully, False if error
388 occurred.
389 """
390 result = True
391 usb_dev = self._probe_host_usb_dev()
392 if not usb_dev:
393 self._logger.error("No usb device connected to servo")
394 return False
395 # Create TempDirectory
396 tmpdir = tempfile.mkdtemp()
397 if tmpdir:
398 # Mount drive to tmpdir.
399 partition_1 = "%s1" % usb_dev
400 rc = subprocess.call(["mount", partition_1, tmpdir])
401 if rc == 0:
402 # Create file 'non_interactive'
403 non_interactive_file = os.path.join(tmpdir, "non_interactive")
404 try:
405 open(non_interactive_file, "w").close()
406 except IOError as e:
407 self._logger.error("Failed to create file %s : %s ( %d )",
408 non_interactive_file, e.strerror, e.errno)
409 result = False
410 except BaseException as e:
411 self._logger.error("Unexpected Exception creating file %s : %s",
412 non_interactive_file, str(e))
413 result = False
414 # Unmount drive regardless if file creation worked or not.
415 rc = subprocess.call(["umount", partition_1])
416 if rc != 0:
417 self._logger.error("Failed to unmount USB Device")
418 result = False
419 else:
420 self._logger.error("Failed to mount USB Device")
421 result = False
422
423 # Delete tmpdir. May throw exception if 'umount' failed.
424 try:
425 os.rmdir(tmpdir)
426 except OSError as e:
427 self._logger.error("Failed to remove temp directory %s : %s",
428 tmpdir, str(e))
429 return False
430 except BaseException as e:
431 self._logger.error("Unexpected Exception removing tempdir %s : %s",
432 tmpdir, str(e))
433 return False
434 else:
435 self._logger.error("Failed to create temp directory.")
436 return False
437 return result
438
Todd Broche505b8d2011-03-21 18:19:54 -0700439 def get(self, name):
440 """Get control value.
441
442 Args:
443 name: name string of control
444
445 Returns:
446 Response from calling drv get method. Value is reformatted based on
447 control's dictionary parameters
448
449 Raises:
450 HwDriverError: Error occurred while using drv
451 """
452 self._logger.debug("name(%s)" % (name))
453 (param, drv) = self._get_param_drv(name)
454 try:
455 val = drv.get()
456 rd_val = self._syscfg.reformat_val(param, val)
Todd Brochb042e7a2011-12-14 17:41:36 -0800457 self._logger.debug("%s = %s" % (name, rd_val))
Todd Broche505b8d2011-03-21 18:19:54 -0700458 return rd_val
Todd Brochfbc499d2011-06-16 16:09:58 -0700459 except AttributeError, error:
460 self._logger.error("Getting %s: %s" % (name, error))
461 raise
Vic Yangbe6cf262012-09-10 10:40:56 +0800462 except HwDriverError:
Todd Broche505b8d2011-03-21 18:19:54 -0700463 self._logger.error("Getting %s" % (name))
464 raise
Todd Brochd6061672012-05-11 15:52:47 -0700465
Todd Broche505b8d2011-03-21 18:19:54 -0700466 def get_all(self, verbose):
467 """Get all controls values.
468
469 Args:
470 verbose: Boolean on whether to return doc info as well
471
472 Returns:
473 string creating from trying to get all values of all controls. In case of
474 error attempting access to control, response is 'ERR'.
475 """
476 rsp = ""
477 for name in self._syscfg.syscfg_dict['control']:
478 self._logger.debug("name = %s" %name)
479 try:
480 value = self.get(name)
481 except Exception:
482 value = "ERR"
483 pass
484 if verbose:
485 rsp += "GET %s = %s :: %s\n" % (name, value, self.doc(name))
486 else:
487 rsp += "%s:%s\n" % (name, value)
488 return rsp
489
490 def set(self, name, wr_val_str):
491 """Set control.
492
493 Args:
494 name: name string of control
495 wr_val_str: value string to write. Can be integer, float or a
496 alpha-numerical that is mapped to a integer or float.
497
498 Raises:
499 HwDriverError: Error occurred while using driver
500 """
Todd Broch7a91c252012-02-03 12:37:45 -0800501 if name == 'sleep':
502 time.sleep(float(wr_val_str))
503 return True
504
Todd Broche505b8d2011-03-21 18:19:54 -0700505 self._logger.debug("name(%s) wr_val(%s)" % (name, wr_val_str))
506 (params, drv) = self._get_param_drv(name, False)
507 wr_val = self._syscfg.resolve_val(params, wr_val_str)
508 try:
509 drv.set(wr_val)
Vic Yangbe6cf262012-09-10 10:40:56 +0800510 except HwDriverError:
Todd Broche505b8d2011-03-21 18:19:54 -0700511 self._logger.error("Setting %s -> %s" % (name, wr_val_str))
512 raise
513 # TODO(tbroch) Figure out why despite allow_none=True for both xmlrpc server
514 # & client I still have to return something to appease the
515 # marshall/unmarshall
516 return True
517
Todd Brochd6061672012-05-11 15:52:47 -0700518 def hwinit(self, verbose=False):
519 """Initialize all controls.
520
521 These values are part of the system config XML files of the form
522 init=<value>. This command should be used by clients wishing to return the
523 servo and DUT its connected to a known good/safe state.
524
525 Args:
526 verbose: boolean, if True prints info about control initialized.
527 Otherwise prints nothing.
528 """
Todd Brochd9acf0a2012-12-05 13:43:06 -0800529 for control_name, value in self._syscfg.hwinit:
Todd Brochd6061672012-05-11 15:52:47 -0700530 self.set(control_name, value)
531 if verbose:
532 self._logger.info('Initialized %s to %s', control_name, value)
Todd Brochd6061672012-05-11 15:52:47 -0700533 return True
534
Todd Broche505b8d2011-03-21 18:19:54 -0700535 def echo(self, echo):
536 """Dummy echo function for testing/examples.
537
538 Args:
539 echo: string to echo back to client
540 """
541 self._logger.debug("echo(%s)" % (echo))
542 return "ECH0ING: %s" % (echo)
543
Todd Brochdbb09982011-10-02 07:14:26 -0700544
Todd Broche505b8d2011-03-21 18:19:54 -0700545def test():
546 """Integration testing.
547
548 TODO(tbroch) Enhance integration test and add unittest (see mox)
549 """
550 logging.basicConfig(level=logging.DEBUG,
551 format="%(asctime)s - %(name)s - " +
552 "%(levelname)s - %(message)s")
553 # configure server & listen
554 servod_obj = Servod(1)
555 # 4 == number of interfaces on a FT4232H device
556 for i in xrange(4):
557 if i == 1:
558 # its an i2c interface ... see __init__ for details and TODO to make
559 # this configureable
560 servod_obj._interface_list[i].wr_rd(0x21, [0], 1)
561 else:
562 # its a gpio interface
563 servod_obj._interface_list[i].wr_rd(0)
564
565 server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 9999),
566 allow_none=True)
567 server.register_introspection_functions()
568 server.register_multicall_functions()
569 server.register_instance(servod_obj)
570 logging.info("Listening on localhost port 9999")
571 server.serve_forever()
572
573if __name__ == "__main__":
574 test()
575
576 # simple client transaction would look like
577 """
578 remote_uri = 'http://localhost:9999'
579 client = xmlrpclib.ServerProxy(remote_uri, verbose=False)
580 send_str = "Hello_there"
581 print "Sent " + send_str + ", Recv " + client.echo(send_str)
582 """