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