1# 2# Copyright 2017-2018 Ettus Research, a National Instruments Company 3# Copyright 2019 Ettus Research, a National Instruments Brand 4# 5# SPDX-License-Identifier: GPL-3.0-or-later 6# 7""" 8Mboard implementation base class 9""" 10 11from __future__ import print_function 12import os 13from hashlib import md5 14from time import sleep 15from concurrent import futures 16from builtins import str 17from builtins import object 18from six import iteritems, itervalues 19from usrp_mpm.mpmlog import get_logger 20from usrp_mpm.sys_utils.udev import get_eeprom_paths 21from usrp_mpm.sys_utils.udev import get_spidev_nodes 22from usrp_mpm.sys_utils import dtoverlay 23from usrp_mpm.sys_utils import net 24from usrp_mpm import eeprom 25from usrp_mpm.rpc_server import no_claim, no_rpc 26from usrp_mpm import prefs 27 28def get_dboard_class_from_pid(pid): 29 """ 30 Given a PID, return a dboard class initializer callable. 31 """ 32 from usrp_mpm import dboard_manager 33 for member in itervalues(dboard_manager.__dict__): 34 try: 35 if issubclass(member, dboard_manager.DboardManagerBase) and \ 36 hasattr(member, 'pids') and \ 37 pid in member.pids: 38 return member 39 except (TypeError, AttributeError): 40 continue 41 return None 42 43 44# We need to disable the no-self-use check, because we might require self to 45# become an RPC method, but PyLint doesnt' know that. We'll also disable 46# warnings about this being a god class. 47# pylint: disable=no-self-use 48# pylint: disable=too-many-public-methods 49# pylint: disable=too-many-instance-attributes 50class PeriphManagerBase(object): 51 """" 52 Base class for all motherboards. Common function and API calls should 53 be implemented here. Motherboard specific information can be stored in 54 separate motherboard classes derived from this class 55 """ 56 ######################################################################### 57 # Overridables 58 # 59 # These values are meant to be overridden by the according subclasses 60 ######################################################################### 61 # Very important: A map of PIDs that apply to the current device. Format is 62 # pid -> product name. If there are multiple products with the same 63 # motherboard PID, use generate_device_info() to update the product key. 64 pids = {} 65 # A textual description of this device type 66 description = "MPM Device" 67 # Address of the motherboard EEPROM. This could be something like 68 # "e0005000.i2c". This value will be passed to get_eeprom_paths() tos 69 # determine a full path to an EEPROM device. 70 # If empty, this will be ignored and no EEPROM info for the device is read 71 # out. 72 mboard_eeprom_addr = "" 73 # Offset of the motherboard EEPROM. All accesses to this EEPROM will be 74 # offset by this amount. In many cases, this value will be 0. But in some 75 # situations, we may want to use the offset as a way of partitioning 76 # access to an EEPROM. 77 mboard_eeprom_offset = 0 78 # The EEPROM code checks for this word to see if the readout was valid. 79 # Typically, devices should not override this unless their EEPROM follows a 80 # different standard. 81 mboard_eeprom_magic = 0xF008AD10 82 # If this value is not set, the code will try and read out the entire EEPROM 83 # content as a binary blob. Use this to limit the number of bytes actually 84 # read. It's usually safe to not override this, as EEPROMs typically aren't 85 # that big. 86 mboard_eeprom_max_len = None 87 # This is the *default* mboard info. The keys from this dict will be copied 88 # into the current device info before it actually gets initialized. This 89 # means that keys from this dict could be overwritten during the 90 # initialization process. 91 mboard_info = {"type": "unknown"} 92 # For checking revision numbers, this is the highest revision that this 93 # particular version of MPM supports. Leave at None to skip a max rev 94 # check. 95 mboard_max_rev = None 96 # A list of available sensors on the motherboard. This dictionary is a map 97 # of the form sensor_name -> method name 98 mboard_sensor_callback_map = {} 99 # This is a sanity check value to see if the correct number of 100 # daughterboards are detected. If somewhere along the line more than 101 # max_num_dboards dboards are found, an error or warning is raised, 102 # depending on the severity of the issue. If fewer dboards are found, 103 # that's generally considered OK. 104 max_num_dboards = 2 105 # The index of the first port of the RFNoC crossbar which is connected to 106 # an RFNoC block 107 crossbar_base_port = 0 108 # Address of the daughterboard EEPROMs. This could be something like 109 # "e0004000.i2c". This value will be passed to get_eeprom_paths() to 110 # determine a full path to an EEPROM device. 111 # If empty, this will be ignored and no EEPROM info for the device is read 112 # out. 113 # If this is a list of EEPROMs, paths will be concatenated. 114 dboard_eeprom_addr = None 115 # Offset of the daughterboard EEPROM. All accesses to this EEPROM will be 116 # offset by this amount. In many cases, this value will be 0. But in some 117 # situations, we may want to use the offset as a way of partitioning 118 # access to an EEPROM. 119 # Assume that all dboard offsets are the same for a given device. That is, 120 # the offset of DBoard 0 == offset of DBoard 1 121 dboard_eeprom_offset = 0 122 # The EEPROM code checks for this word to see if the readout was valid. 123 # Typically, devices should not override this unless their EEPROM follows a 124 # different standard. 125 dboard_eeprom_magic = 0xF008AD11 126 # If this value is not set, the code will try and read out the entire EEPROM 127 # content as a binary blob. Use this to limit the number of bytes actually 128 # read. It's usually safe to not override this, as EEPROMs typically aren't 129 # that big. 130 dboard_eeprom_max_len = None 131 # If the dboard requires spidev access, the following attribute is a list 132 # of SPI master addrs (typically something like 'e0006000.spi'). You 133 # usually want the length of this list to be as long as the number of 134 # dboards, but if it's shorter, it simply won't instantiate list SPI nodes 135 # for those dboards. 136 dboard_spimaster_addrs = [] 137 # Dictionary containing valid IDs for the update_component function for a 138 # specific implementation. Each PeriphManagerBase-derived class should list 139 # information required to update the component, like a callback function 140 updateable_components = {} 141 # The RPC server checks this value to determine if it needs to clear 142 # the RPC method registry. This is typically to remove stale references 143 # to RPC methods caused by removal of overlay on unclaim() by peripheral 144 # manager. Additionally the RPC server will re-register all methods on 145 # a claim(). Override and set to True in the derived class if desired. 146 clear_rpc_registry_on_unclaim = False 147 148 # Disable checks for unused args in the overridables, because the default 149 # implementations don't need to use them. 150 # pylint: disable=unused-argument 151 @staticmethod 152 def generate_device_info(eeprom_md, mboard_info, dboard_infos): 153 """ 154 Returns a dictionary which describes the device. 155 156 mboard_info -- Dictionary; motherboard info 157 device_args -- List of dictionaries; daughterboard info 158 """ 159 # Try to add the MPM Git hash and version 160 try: 161 from usrp_mpm import __version__, __githash__ 162 version_string = __version__ 163 if __githash__: 164 version_string += "-g" + str(__githash__) 165 except ImportError: 166 version_string = "" 167 mboard_info["mpm_sw_version"] = version_string 168 169 try: 170 with open("/etc/version", "r") as version_file: 171 mboard_info["fs_version"] = version_file.read().strip(" \r\n") 172 except FileNotFoundError: 173 mboard_info["fs_version"] = "FILE NOT FOUND" 174 175 try: 176 with open("/etc/mender/artifact_info", "r") as artifact_file: 177 for line in artifact_file.read().splitlines(): 178 if line.startswith('artifact_name='): 179 mboard_info['mender_artifact'] = line[14:] 180 except FileNotFoundError: 181 mboard_info['mender_artifact'] = "FILE NOT FOUND" 182 183 for i,dboard_info in enumerate(dboard_infos): 184 mboard_info["dboard_{}_pid".format(i)] = str(dboard_info["pid"]) 185 mboard_info["dboard_{}_serial".format(i)] = dboard_info["eeprom_md"]["serial"] 186 187 return mboard_info 188 189 @staticmethod 190 # Yes, this is overridable too: List the required device tree overlays 191 def list_required_dt_overlays(device_info): 192 """ 193 Lists device tree overlays that need to be applied before this class can 194 be used. List of strings. 195 Are applied in order. 196 197 eeprom_md -- Dictionary of info read out from the mboard EEPROM 198 device_args -- Arbitrary dictionary of info, typically user-defined 199 """ 200 return [] 201 # pylint: enable=unused-argument 202 ### End of overridables ################################################### 203 204 205 ########################################################################### 206 # Device initialization (at MPM startup) 207 ########################################################################### 208 def __init__(self): 209 # This gets set in the child class 210 self.mboard_regs_control = None 211 # Note: args is a dictionary. 212 assert self.pids 213 assert self.mboard_eeprom_magic is not None 214 self.dboards = [] 215 self._default_args = "" 216 # Set up logging 217 self.log = get_logger('PeriphManager') 218 self.claimed = False 219 try: 220 self._eeprom_head, self._eeprom_rawdata = \ 221 self._read_mboard_eeprom() 222 self.mboard_info = self._get_mboard_info(self._eeprom_head) 223 self.log.info("Device serial number: {}" 224 .format(self.mboard_info.get('serial', 'n/a'))) 225 self.dboard_infos = self._get_dboard_eeprom_info() 226 self.device_info = \ 227 self.generate_device_info( 228 self._eeprom_head, 229 self.mboard_info, 230 self.dboard_infos 231 ) 232 except BaseException as ex: 233 self.log.error("Failed to initialize device: %s", str(ex)) 234 self._device_initialized = False 235 self._initialization_status = str(ex) 236 super(PeriphManagerBase, self).__init__() 237 238 def overlay_apply(self): 239 """ 240 Apply FPGA overlay 241 """ 242 self._init_mboard_overlays() 243 244 def init_dboards(self, args): 245 """ 246 Run full initialization of daughter boards if they exist. 247 Use 'override_db_pids' args to overwrite number of dboards that get 248 initialized. 249 """ 250 self._default_args = self._update_default_args(args) 251 self.log.debug("Using default args: {}".format(self._default_args)) 252 override_db_pids_str = self._default_args.get('override_db_pids') 253 if override_db_pids_str: 254 override_db_pids = [ 255 int(x, 0) for x in override_db_pids_str.split(",") 256 ] 257 else: 258 override_db_pids = [] 259 self._init_dboards( 260 self.dboard_infos, 261 override_db_pids, 262 self._default_args 263 ) 264 self._device_initialized = True 265 self._initialization_status = "No errors." 266 267 def _read_mboard_eeprom(self): 268 """ 269 Read out mboard EEPROM. 270 Returns a tuple: (eeprom_dict, eeprom_rawdata), where the the former is 271 a de-serialized dictionary representation of the data, and the latter 272 is a binary string with the raw data. 273 274 If no EEPROM is defined, returns empty values. 275 """ 276 if self.mboard_eeprom_addr: 277 self.log.trace("Reading EEPROM from address `{}'..." 278 .format(self.mboard_eeprom_addr)) 279 eeprom_paths = get_eeprom_paths(self.mboard_eeprom_addr) 280 if not eeprom_paths: 281 self.log.error("Could not identify EEPROM paths for %s!", 282 self.mboard_eeprom_addr) 283 return {}, b'' 284 self.log.trace("Found mboard EEPROM path: %s", eeprom_paths[0]) 285 (eeprom_head, eeprom_rawdata) = eeprom.read_eeprom( 286 eeprom_paths[0], 287 self.mboard_eeprom_offset, 288 eeprom.MboardEEPROM.eeprom_header_format, 289 eeprom.MboardEEPROM.eeprom_header_keys, 290 self.mboard_eeprom_magic, 291 self.mboard_eeprom_max_len, 292 ) 293 self.log.trace("Found EEPROM metadata: `{}'" 294 .format(str(eeprom_head))) 295 self.log.trace("Read {} bytes of EEPROM data." 296 .format(len(eeprom_rawdata))) 297 return eeprom_head, eeprom_rawdata 298 # Nothing defined? Return defaults. 299 self.log.trace("No mboard EEPROM path defined. " 300 "Skipping mboard EEPROM readout.") 301 return {}, b'' 302 303 def _get_mboard_info(self, eeprom_head): 304 """ 305 Creates the mboard info dictionary from the EEPROM data. 306 """ 307 mboard_info = self.mboard_info 308 if not eeprom_head: 309 self.log.debug("No EEPROM info: Can't generate mboard_info") 310 return mboard_info 311 for key in ('pid', 'serial', 'rev', 'eeprom_version'): 312 # In C++, we can only handle dicts if all the values are of the 313 # same type. So we must convert them all to strings here: 314 try: 315 mboard_info[key] = str(eeprom_head.get(key, ''), 'ascii') 316 except TypeError: 317 mboard_info[key] = str(eeprom_head.get(key, '')) 318 if 'pid' in eeprom_head: 319 if eeprom_head['pid'] not in self.pids.keys(): 320 self.log.error( 321 "Found invalid PID in EEPROM: 0x{:04X}. " \ 322 "Valid PIDs are: {}".format( 323 eeprom_head['pid'], 324 ", ".join(["0x{:04X}".format(x) for x in self.pids]), 325 ) 326 ) 327 raise RuntimeError("Invalid PID found in EEPROM.") 328 # The rev_compat is either directly stored in the EEPROM, or we fall 329 # back to the the rev itself (because every rev is compatible with 330 # itself). 331 rev_compat = \ 332 eeprom_head.get('rev_compat', eeprom_head.get('rev')) 333 try: 334 rev_compat = int(rev_compat) 335 except (ValueError, TypeError): 336 raise RuntimeError( 337 "Invalid revision compat info read from EEPROM!" 338 ) 339 # We check if this software is actually compatible with the hardware. 340 # In order for the software to be able to understand the hardware, the 341 # rev_compat value (stored on the EEPROM) must be smaller or equal to 342 # the value stored in the software itself. 343 if self.mboard_max_rev is None: 344 self.log.warning("Skipping HW/SW compatibility check!") 345 else: 346 if rev_compat > self.mboard_max_rev: 347 raise RuntimeError( 348 "Software is maximally compatible with revision `{}', but " 349 "the hardware has revision `{}' and is minimally compatible " 350 "with hardware revision `{}'. Please upgrade your version of" 351 "MPM in order to use this device." 352 .format(self.mboard_max_rev, mboard_info['rev'], rev_compat) 353 ) 354 return mboard_info 355 356 def _get_dboard_eeprom_info(self): 357 """ 358 Read back EEPROM info from the daughterboards 359 """ 360 if self.dboard_eeprom_addr is None: 361 self.log.debug("No dboard EEPROM addresses given.") 362 return [] 363 dboard_eeprom_addrs = self.dboard_eeprom_addr \ 364 if isinstance(self.dboard_eeprom_addr, list) \ 365 else [self.dboard_eeprom_addr] 366 dboard_eeprom_paths = [] 367 self.log.trace("Identifying dboard EEPROM paths from addrs `{}'..." 368 .format(",".join(dboard_eeprom_addrs))) 369 for dboard_eeprom_addr in dboard_eeprom_addrs: 370 self.log.trace("Resolving %s...", dboard_eeprom_addr) 371 dboard_eeprom_paths += get_eeprom_paths(dboard_eeprom_addr) 372 self.log.trace("Found dboard EEPROM paths: {}" 373 .format(",".join(dboard_eeprom_paths))) 374 if len(dboard_eeprom_paths) > self.max_num_dboards: 375 self.log.warning("Found more EEPROM paths than daughterboards. " 376 "Ignoring some of them.") 377 dboard_eeprom_paths = dboard_eeprom_paths[:self.max_num_dboards] 378 dboard_info = [] 379 for dboard_idx, dboard_eeprom_path in enumerate(dboard_eeprom_paths): 380 self.log.debug("Reading EEPROM info for dboard %d...", dboard_idx) 381 dboard_eeprom_md, dboard_eeprom_rawdata = eeprom.read_eeprom( 382 dboard_eeprom_path, 383 self.dboard_eeprom_offset, 384 eeprom.DboardEEPROM.eeprom_header_format, 385 eeprom.DboardEEPROM.eeprom_header_keys, 386 self.dboard_eeprom_magic, 387 self.dboard_eeprom_max_len, 388 ) 389 self.log.trace("Found dboard EEPROM metadata: `{}'" 390 .format(str(dboard_eeprom_md))) 391 self.log.trace("Read %d bytes of dboard EEPROM data.", 392 len(dboard_eeprom_rawdata)) 393 db_pid = dboard_eeprom_md.get('pid') 394 if db_pid is None: 395 self.log.warning("No dboard PID found in dboard EEPROM!") 396 else: 397 self.log.debug("Found dboard PID in EEPROM: 0x{:04X}" 398 .format(db_pid)) 399 dboard_info.append({ 400 'eeprom_md': dboard_eeprom_md, 401 'eeprom_rawdata': dboard_eeprom_rawdata, 402 'pid': db_pid, 403 }) 404 return dboard_info 405 406 def _update_default_args(self, default_args): 407 """ 408 Pipe the default_args (that get passed into us from the RPC server) 409 through the prefs API. This way, we respect both the config file and 410 command line arguments. 411 """ 412 prefs_cache = prefs.get_prefs() 413 periph_section_name = None 414 if prefs_cache.has_section(self.device_info.get('product')): 415 periph_section_name = self.device_info.get('product') 416 elif prefs_cache.has_section(self.device_info.get('type')): 417 periph_section_name = self.device_info.get('type') 418 if periph_section_name is not None: 419 prefs_cache.read_dict({periph_section_name: default_args}) 420 return dict(prefs_cache[periph_section_name]) 421 # else: 422 return default_args 423 424 def _init_mboard_overlays(self): 425 """ 426 Load all required overlays for this motherboard 427 """ 428 requested_overlays = self.list_required_dt_overlays( 429 self.device_info, 430 ) 431 self.log.debug("Motherboard requests device tree overlays: {}".format( 432 requested_overlays 433 )) 434 for overlay in requested_overlays: 435 dtoverlay.apply_overlay_safe(overlay) 436 # Need to wait here a second to make sure the ethernet interfaces are up 437 # TODO: Fine-tune this number, or wait for some smarter signal. 438 sleep(1) 439 440 def _init_dboards(self, dboard_infos, override_dboard_pids, default_args): 441 """ 442 Initialize all the daughterboards 443 444 dboard_infos -- List of dictionaries as returned from 445 _get_dboard_eeprom_info() 446 override_dboard_pids -- List of dboard PIDs to force 447 default_args -- Default args 448 """ 449 if override_dboard_pids: 450 self.log.warning("Overriding daughterboard PIDs with: {}" 451 .format(",".join(override_dboard_pids))) 452 assert len(dboard_infos) <= self.max_num_dboards 453 if override_dboard_pids and \ 454 len(override_dboard_pids) < len(dboard_infos): 455 self.log.warning("--override-db-pids is going to skip dboards.") 456 dboard_infos = dboard_infos[:len(override_dboard_pids)] 457 for dboard_idx, dboard_info in enumerate(dboard_infos): 458 self.log.debug("Initializing dboard %d...", dboard_idx) 459 db_pid = dboard_info.get('pid') 460 db_class = get_dboard_class_from_pid(db_pid) 461 if db_class is None: 462 self.log.warning("Could not identify daughterboard class " 463 "for PID {:04X}! Skipping.".format(db_pid)) 464 continue 465 if len(self.dboard_spimaster_addrs) > dboard_idx: 466 spi_nodes = sorted(get_spidev_nodes( 467 self.dboard_spimaster_addrs[dboard_idx])) 468 self.log.trace("Found spidev nodes: {0}".format(spi_nodes)) 469 else: 470 spi_nodes = [] 471 self.log.warning("No SPI nodes for dboard %d.", dboard_idx) 472 dboard_info.update({ 473 'spi_nodes': spi_nodes, 474 'default_args': default_args, 475 }) 476 # This will actually instantiate the dboard class: 477 self.dboards.append(db_class(dboard_idx, **dboard_info)) 478 self.log.info("Initialized %d daughterboard(s).", len(self.dboards)) 479 480 ########################################################################### 481 # Session (de-)initialization (at UHD startup) 482 ########################################################################### 483 def init(self, args): 484 """ 485 Run the mboard initialization. This is typically done at the beginning 486 of a UHD session. 487 Default behaviour is to call init() on all the daughterboards.`args' is 488 passed to the daughterboard's init calls. For additional features, 489 this needs to be overridden. 490 491 The main requirement of this function is, after calling it successfully, 492 all RFNoC blocks must be reachable via CHDR interfaces (i.e., clocks 493 need to be on). 494 495 Return False on failure, True on success. If daughterboard inits return 496 False (any of them), this will also return False. 497 498 args -- A dictionary of args for initialization. Similar to device args 499 in UHD. 500 """ 501 self.log.info("init() called with device args `{}'.".format( 502 ",".join(['{}={}'.format(x, args[x]) for x in args]) 503 )) 504 if not self._device_initialized: 505 self.log.error( 506 "Cannot run init(), device was never fully initialized!") 507 return False 508 if not self.dboards: 509 return True 510 if args.get("serialize_init", False): 511 self.log.debug("Initializing dboards serially...") 512 return all((dboard.init(args) for dboard in self.dboards)) 513 self.log.debug("Initializing dboards in parallel...") 514 num_workers = len(self.dboards) 515 with futures.ThreadPoolExecutor(max_workers=num_workers) as executor: 516 init_futures = [ 517 executor.submit(dboard.init, args) 518 for dboard in self.dboards 519 ] 520 return all([ 521 x.result() 522 for x in futures.as_completed(init_futures) 523 ]) 524 525 def deinit(self): 526 """ 527 Clean up after a UHD session terminates. 528 This must be safe to call multiple times. The default behaviour is to 529 call deinit() on all the daughterboards. 530 """ 531 if not self._device_initialized: 532 self.log.error( 533 "Cannot run deinit(), device was never fully initialized!") 534 return 535 self.log.trace("Mboard deinit() called.") 536 for dboard in self.dboards: 537 dboard.deinit() 538 539 def tear_down(self): 540 """ 541 Tear down all members that need to be specially handled before 542 deconstruction. 543 """ 544 self.log.trace("Teardown called for Peripheral Manager base.") 545 546 ########################################################################### 547 # RFNoC & Device Info 548 ########################################################################### 549 def set_device_id(self, device_id): 550 """ 551 Sets the device ID for this motherboard. 552 The device ID is used to identify the RFNoC components associated with 553 this motherboard. 554 """ 555 self.log.debug("Setting device ID to `{}'".format(device_id)) 556 self.mboard_regs_control.set_device_id(device_id) 557 558 def get_device_id(self): 559 """ 560 Gets the device ID for this motherboard. 561 The device ID is used to identify the RFNoC components associated with 562 this motherboard. 563 """ 564 return self.mboard_regs_control.get_device_id() 565 566 @no_claim 567 def get_proto_ver(self): 568 """ 569 Return RFNoC protocol version 570 """ 571 proto_ver = self.mboard_regs_control.get_proto_ver() 572 self.log.debug("RFNoC protocol version supported by this device is {}".format(proto_ver)) 573 return proto_ver 574 575 @no_claim 576 def get_chdr_width(self): 577 """ 578 Return RFNoC CHDR width 579 """ 580 chdr_width = self.mboard_regs_control.get_chdr_width() 581 self.log.debug("CHDR width supported by the device is {}".format(chdr_width)) 582 return chdr_width 583 584 ########################################################################### 585 # Misc device status controls and indicators 586 ########################################################################### 587 def get_init_status(self): 588 """ 589 Returns the status of the device after its initialization (that happens 590 at startup, not that happens when init() is called). 591 The status is a tuple of 2 strings, the first is either "true" or 592 "false", depending on whether or not the device initialization was 593 successful, and the second is an arbitrary error string. 594 595 Use this function to figure out if something went wrong at bootup, and 596 what. 597 """ 598 return [ 599 "true" if self._device_initialized else "false", 600 self._initialization_status 601 ] 602 603 @no_claim 604 def list_available_overlays(self): 605 """ 606 Returns a list of available device tree overlays 607 """ 608 return dtoverlay.list_available_overlays() 609 610 @no_claim 611 def list_active_overlays(self): 612 """ 613 Returns a list of currently loaded device tree overlays 614 check which dt overlay is loaded currently 615 """ 616 return dtoverlay.list_overlays() 617 618 @no_rpc 619 def get_device_info(self): 620 """ 621 Return the device_info dict and add a claimed field. 622 623 Will also call into get_device_info_dyn() for additional information. 624 Don't override this function. 625 """ 626 result = {"claimed": str(self.claimed)} 627 result.update(self.device_info) 628 result.update({ 629 'name': net.get_hostname(), 630 'description': self.description, 631 }) 632 result.update(self.get_device_info_dyn()) 633 return result 634 635 @no_rpc 636 def get_device_info_dyn(self): 637 """ 638 "Dynamic" device info getter. When get_device_info() is called, it 639 will also call into this function to see if there is 'dynamic' info 640 that needs to be returned. The reason to split up these functions is 641 because we don't want anyone to override get_device_info(), but we do 642 want periph managers to be able to inject custom device info data. 643 """ 644 self.log.trace("Called get_device_info_dyn(), but not implemented.") 645 return {} 646 647 @no_rpc 648 def set_connection_type(self, conn_type): 649 """ 650 Specify how the RPC client has connected to this MPM instance. Valid 651 values are "remote", "local", or None. When None is given, the value 652 is reset. 653 """ 654 assert conn_type in ('remote', 'local', None) 655 if conn_type is None: 656 self.device_info.pop('rpc_connection', None) 657 else: 658 self.device_info['rpc_connection'] = conn_type 659 660 @no_claim 661 def get_dboard_info(self): 662 """ 663 Returns a list of dicts. One dict per dboard. 664 """ 665 return [dboard.device_info for dboard in self.dboards] 666 667 ########################################################################### 668 # Component updating 669 ########################################################################### 670 @no_claim 671 def list_updateable_components(self): 672 """ 673 return list of updateable components 674 This method does not require a claim_token in the RPC 675 """ 676 return list(self.updateable_components.keys()) 677 678 def update_component(self, metadata_l, data_l): 679 """ 680 Updates the device component specified by comp_dict 681 :param metadata_l: List of dictionary of strings containing metadata 682 :param data_l: List of binary string with the file contents to be written 683 """ 684 # We need a 'metadata' and a 'data' for each file we want to update 685 assert (len(metadata_l) == len(data_l)),\ 686 "update_component arguments must be the same length" 687 # Iterate through the components, updating each in turn 688 for metadata, data in zip(metadata_l, data_l): 689 id_str = metadata['id'] 690 filename = os.path.basename(metadata['filename']) 691 if id_str not in self.updateable_components: 692 self.log.error("{0} not an updateable component ({1})".format( 693 id_str, self.updateable_components.keys() 694 )) 695 raise KeyError("Update component not implemented for {}".format(id_str)) 696 self.log.trace("Updating component: {}".format(id_str)) 697 if 'md5' in metadata: 698 given_hash = metadata['md5'] 699 comp_hash = md5() 700 comp_hash.update(data) 701 comp_hash = comp_hash.hexdigest() 702 if comp_hash == given_hash: 703 self.log.trace("Component file hash matched: {}".format( 704 comp_hash 705 )) 706 else: 707 self.log.error("Component file hash mismatched:\n" 708 "Calculated {}\n" 709 "Given {}\n".format( 710 comp_hash, given_hash)) 711 raise RuntimeError("Component file hash mismatch") 712 else: 713 self.log.trace("Loading unverified {} image.".format( 714 id_str 715 )) 716 basepath = os.path.join(os.sep, "tmp", "uploads") 717 filepath = os.path.join(basepath, filename) 718 if not os.path.isdir(basepath): 719 self.log.trace("Creating directory {}".format(basepath)) 720 os.makedirs(basepath) 721 self.log.trace("Writing data to {}".format(filepath)) 722 with open(filepath, 'wb') as comp_file: 723 comp_file.write(data) 724 update_func = \ 725 getattr(self, self.updateable_components[id_str]['callback']) 726 self.log.info("Updating component `%s'", id_str) 727 update_func(filepath, metadata) 728 return True 729 730 @no_claim 731 def get_component_info(self, component_name): 732 """ 733 Returns the metadata for the requested component 734 :param component_name: string name of the component 735 :return: Dictionary of strings containg metadata 736 """ 737 if component_name in self.updateable_components: 738 metadata = self.updateable_components.get(component_name) 739 metadata['id'] = component_name 740 self.log.trace("Component info: {}".format(metadata)) 741 # Convert all values to str 742 return dict([a, str(x)] for a, x in metadata.items()) 743 # else: 744 self.log.trace("Component not found in updateable components: {}" 745 .format(component_name)) 746 return {} 747 748 ########################################################################## 749 # Mboard Sensors 750 ########################################################################## 751 def get_mb_sensors(self): 752 """ 753 Return a list of sensor names. 754 """ 755 return list(self.mboard_sensor_callback_map.keys()) 756 757 def get_mb_sensor(self, sensor_name): 758 """ 759 Return a dictionary that represents the sensor values for a given 760 sensor. If the requested sensor sensor_name does not exist, throw an 761 exception. 762 763 The returned dictionary has the following keys (all values are 764 strings): 765 - name: This is typically the same as sensor_name 766 - type: One of the following strings: BOOLEAN, INTEGER, REALNUM, STRING 767 Note that this matches uhd::sensor_value_t::data_type_t 768 - value: The value. If type is STRING, it is interpreted as-is. If it's 769 REALNUM or INTEGER, it needs to be convertable to float or 770 int, respectively. If it's BOOLEAN, it needs to be either 771 'true' or 'false', although any string that is not 'true' will 772 be interpreted as false. 773 - unit: This depends on the type. It is generally only relevant for 774 pretty-printing the sensor value. 775 """ 776 if sensor_name not in self.get_mb_sensors(): 777 error_msg = "Was asked for non-existent sensor `{}'.".format( 778 sensor_name 779 ) 780 self.log.error(error_msg) 781 raise RuntimeError(error_msg) 782 return getattr( 783 self, self.mboard_sensor_callback_map.get(sensor_name) 784 )() 785 786 ########################################################################## 787 # EEPROMS 788 ########################################################################## 789 def get_mb_eeprom(self): 790 """ 791 Return a dictionary with EEPROM contents 792 793 All key/value pairs are string -> string 794 """ 795 return {k: str(v) for k, v in iteritems(self._eeprom_head)} 796 797 def set_mb_eeprom(self, eeprom_vals): 798 """ 799 eeprom_vals is a dictionary (string -> string) 800 801 By default, we do nothing. Writing EEPROMs is highly device specific 802 and is thus defined in the individual device classes. 803 """ 804 self.log.warn("Called set_mb_eeprom(), but not implemented!") 805 self.log.debug("Skipping writing EEPROM keys: {}" 806 .format(list(eeprom_vals.keys()))) 807 808 def get_db_eeprom(self, dboard_idx): 809 """ 810 Return a dictionary representing the content of the daughterboard 811 EEPROM. 812 813 By default, will simply return the device info of the dboard. 814 Typically, this gets overloaded by the device specific class. 815 816 Arguments: 817 dboard_idx -- Slot index of dboard 818 """ 819 self.log.debug("Calling base-class get_db_eeprom(). This may not be " \ 820 "what you want.") 821 return self.dboards[dboard_idx].device_info 822 823 def set_db_eeprom(self, dboard_idx, eeprom_data): 824 """ 825 Write new EEPROM contents with eeprom_map. 826 827 Arguments: 828 dboard_idx -- Slot index of dboard 829 eeprom_data -- Dictionary of EEPROM data to be written. It's up to the 830 specific device implementation on how to handle it. 831 """ 832 self.log.warn("Attempted to write dboard `%d' EEPROM, but function " \ 833 "is not implemented.", dboard_idx) 834 self.log.debug("Skipping writing EEPROM keys: {}" 835 .format(list(eeprom_data.keys()))) 836 837 ####################################################################### 838 # Transport API 839 ####################################################################### 840 def get_chdr_link_types(self): 841 """ 842 Return a list of ways how the UHD session can connect to this device to 843 initiate CHDR traffic. 844 845 The return value is a list of strings. Every string is a key for a 846 transport type. Values include: 847 - "udp": Means this device can be reached via UDP 848 849 The list is filtered based on what the device knows about where the UHD 850 session is. For example, on an N310, it will only return "UDP". 851 852 In order to get further information about how to connect to the device, 853 the keys returned from this function can be used with 854 get_chdr_link_options(). 855 """ 856 raise NotImplementedError("get_chdr_link_types() not implemented.") 857 858 def get_chdr_link_options(self, xport_type): 859 """ 860 Returns a list of dictionaries. Every dictionary contains information 861 about one way to connect to this device in order to initiate CHDR 862 traffic. 863 864 The interpretation of the return value is very highly dependant on the 865 transport type (xport_type). 866 For UDP, the every entry of the list has the following keys: 867 - ipv4 (IP Address) 868 - port (UDP port) 869 - link_rate (bps of the link, e.g. 10e9 for 10GigE) 870 871 """ 872 raise NotImplementedError("get_chdr_link_options() not implemented.") 873 874 ####################################################################### 875 # Claimer API 876 ####################################################################### 877 def claim(self): 878 """ 879 This is called when the device is claimed, in case the device needs to 880 run any actions on claiming (e.g., light up an LED). 881 882 Consider this a "post claim hook", not a function to actually claim 883 this device (which happens outside of this class). 884 """ 885 self.log.trace("Device was claimed. No actions defined.") 886 887 def unclaim(self): 888 """ 889 This is called when the device is unclaimed, in case the device needs 890 to run any actions on claiming (e.g., turn off an LED). 891 892 Consider this a "post unclaim hook", not a function to actually 893 unclaim this device (which happens outside of this class). 894 """ 895 self.log.debug("Device was unclaimed. No actions defined.") 896 897 ####################################################################### 898 # Timekeeper API 899 ####################################################################### 900 def get_num_timekeepers(self): 901 """ 902 Return the number of timekeepers 903 """ 904 return self.mboard_regs_control.get_num_timekeepers() 905 906 def get_timekeeper_time(self, tk_idx, last_pps): 907 """ 908 Get the time in ticks 909 910 Arguments: 911 tk_idx: Index of timekeeper 912 next_pps: If True, get time at last PPS. Otherwise, get time now. 913 """ 914 return self.mboard_regs_control.get_timekeeper_time(tk_idx, last_pps) 915 916 def set_timekeeper_time(self, tk_idx, ticks, next_pps): 917 """ 918 Set the time in ticks 919 920 Arguments: 921 tk_idx: Index of timekeeper 922 ticks: Time in ticks 923 next_pps: If True, set time at next PPS. Otherwise, set time now. 924 """ 925 self.mboard_regs_control.set_timekeeper_time(tk_idx, ticks, next_pps) 926 927 def set_tick_period(self, tk_idx, period_ns): 928 """ 929 Set the time per tick in nanoseconds (tick period) 930 931 Arguments: 932 tk_idx: Index of timekeeper 933 period_ns: Period in nanoseconds 934 """ 935 self.mboard_regs_control.set_tick_period(tk_idx, period_ns) 936 937 def get_clocks(self): 938 """ 939 Gets the RFNoC-related clocks present in the FPGA design 940 """ 941 raise NotImplementedError("get_clocks() not implemented.") 942 943 ####################################################################### 944 # GPIO API 945 ####################################################################### 946 def get_gpio_banks(self): 947 """ 948 Returns a list of GPIO banks over which MPM has any control 949 """ 950 self.log.debug("get_gpio_banks(): No banks defined on this device.") 951 return [] 952 953 def get_gpio_srcs(self, bank): 954 """ 955 Return a list of valid GPIO sources for a given bank 956 """ 957 assert bank in self.get_gpio_banks(), \ 958 "Invalid GPIO bank: {}".format(bank) 959 return [] 960 961 def get_gpio_src(self, bank): 962 """ 963 Return the currently selected GPIO source for a given bank. The return 964 value is a list of strings. The length of the vector is identical to 965 the number of controllable GPIO pins on this bank. 966 """ 967 assert bank in self.get_gpio_banks(), \ 968 "Invalid GPIO bank: {}".format(bank) 969 raise NotImplementedError("get_gpio_src() not available on this device!") 970 971 def set_gpio_src(self, bank, src): 972 """ 973 Set the GPIO source for a given bank. 974 """ 975 assert bank in self.get_gpio_banks(), \ 976 "Invalid GPIO bank: {}".format(bank) 977 assert src in self.get_gpio_srcs(bank), \ 978 "Invalid GPIO source: {}".format(src) 979 raise NotImplementedError("set_gpio_src() not available on this device!") 980