mirror of
https://github.com/saymrwulf/uhd.git
synced 2026-05-16 21:10:10 +00:00
- Sort methods by functional groups - Reorder init from less likely to fail to more likely (this enables the LINK LED on claim for when FPGA fails to initialize, e.g. on compat errors)
1159 lines
43 KiB
Python
1159 lines
43 KiB
Python
#
|
|
# Copyright 2017 Ettus Research, National Instruments Company
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0
|
|
#
|
|
"""
|
|
N310 implementation module
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import os
|
|
import copy
|
|
import shutil
|
|
import subprocess
|
|
import json
|
|
import time
|
|
import datetime
|
|
import threading
|
|
from six import iteritems, itervalues
|
|
from builtins import object
|
|
from usrp_mpm.gpsd_iface import GPSDIface
|
|
from usrp_mpm.periph_manager import PeriphManagerBase
|
|
from usrp_mpm.mpmtypes import SID
|
|
from usrp_mpm.mpmutils import assert_compat_number
|
|
from usrp_mpm.rpc_server import no_rpc
|
|
from usrp_mpm.sys_utils import dtoverlay
|
|
from usrp_mpm.sys_utils.sysfs_gpio import SysFSGPIO, GPIOBank
|
|
from usrp_mpm.sys_utils.uio import UIO
|
|
from usrp_mpm.sys_utils.sysfs_thermal import read_thermal_sensor_value
|
|
from usrp_mpm.xports import XportMgrUDP, XportMgrLiberio
|
|
|
|
N3XX_DEFAULT_EXT_CLOCK_FREQ = 10e6
|
|
N3XX_DEFAULT_CLOCK_SOURCE = 'internal'
|
|
N3XX_DEFAULT_TIME_SOURCE = 'internal'
|
|
N3XX_DEFAULT_ENABLE_GPS = True
|
|
N3XX_DEFAULT_ENABLE_FPGPIO = True
|
|
N3XX_FPGA_COMPAT = (1, 0)
|
|
N3XX_MONITOR_THREAD_INTERVAL = 1.0 # seconds
|
|
|
|
###############################################################################
|
|
# Additional peripheral controllers specific to Magnesium
|
|
###############################################################################
|
|
class TCA6424(object):
|
|
"""
|
|
Abstraction layer for the port/gpio expander
|
|
pins_list is an array of different version of TCA6424 pins map.
|
|
First element of this array corresponding to revC, second is revD etc...
|
|
"""
|
|
pins_list = [
|
|
(
|
|
'PWREN-CLK-MGT156MHz',
|
|
'NETCLK-CE', #revC name: 'PWREN-CLK-WB-CDCM',
|
|
'NETCLK-RESETn', #revC name: 'WB-CDCM-RESETn',
|
|
'NETCLK-PR0', #revC name: 'WB-CDCM-PR0',
|
|
'NETCLK-PR1', #revC name: 'WB-CDCM-PR1',
|
|
'NETCLK-OD0', #revC name: 'WB-CDCM-OD0',
|
|
'NETCLK-OD1', #revC name: 'WB-CDCM-OD1',
|
|
'NETCLK-OD2', #revC name: 'WB-CDCM-OD2',
|
|
'PWREN-CLK-MAINREF',
|
|
'CLK-MAINSEL-25MHz', #revC name: 'CLK-MAINREF-SEL1',
|
|
'CLK-MAINSEL-EX_B', #revC name: 'CLK-MAINREF-SEL0',
|
|
'12',
|
|
'CLK-MAINSEL-GPS', #revC name: '13',
|
|
'FPGA-GPIO-EN',
|
|
'PWREN-CLK-WB-20MHz',
|
|
'PWREN-CLK-WB-25MHz',
|
|
'GPS-PHASELOCK',
|
|
'GPS-nINITSURV',
|
|
'GPS-nRESET',
|
|
'GPS-WARMUP',
|
|
'GPS-SURVEY',
|
|
'GPS-LOCKOK',
|
|
'GPS-ALARM',
|
|
'PWREN-GPS',
|
|
),
|
|
(
|
|
'NETCLK-PR1',
|
|
'NETCLK-PR0',
|
|
'NETCLK-CE',
|
|
'NETCLK-RESETn',
|
|
'NETCLK-OD2',
|
|
'NETCLK-OD1',
|
|
'NETCLK-OD0',
|
|
'PWREN-CLK-MGT156MHz',
|
|
'PWREN-CLK-MAINREF',
|
|
'CLK-MAINSEL-25MHz',
|
|
'CLK-MAINSEL-EX_B',
|
|
'12',
|
|
'CLK-MAINSEL-GPS',
|
|
'FPGA-GPIO-EN',
|
|
'PWREN-CLK-WB-20MHz',
|
|
'PWREN-CLK-WB-25MHz',
|
|
'GPS-PHASELOCK',
|
|
'GPS-nINITSURV',
|
|
'GPS-nRESET',
|
|
'GPS-WARMUP',
|
|
'GPS-SURVEY',
|
|
'GPS-LOCKOK',
|
|
'GPS-ALARM',
|
|
'PWREN-GPS',
|
|
)]
|
|
|
|
def __init__(self, rev):
|
|
# Default state: Turn on GPS power, take GPS out of reset or
|
|
# init-survey, turn on 156.25 MHz clock
|
|
# min Support from revC or rev = 2
|
|
if rev == 2:
|
|
self.pins = self.pins_list[0]
|
|
else:
|
|
self.pins = self.pins_list[1]
|
|
|
|
default_val = 0x860101 if rev == 2 else 0x860780
|
|
self._gpios = SysFSGPIO('tca6424', 0xFFF7FF, 0x86F7FF, default_val)
|
|
|
|
def set(self, name, value=None):
|
|
"""
|
|
Assert a pin by name
|
|
"""
|
|
assert name in self.pins
|
|
self._gpios.set(self.pins.index(name), value=value)
|
|
|
|
def reset(self, name):
|
|
"""
|
|
Deassert a pin by name
|
|
"""
|
|
self.set(name, value=0)
|
|
|
|
def get(self, name):
|
|
"""
|
|
Read back a pin by name
|
|
"""
|
|
assert name in self.pins
|
|
return self._gpios.get(self.pins.index(name))
|
|
|
|
|
|
class FrontpanelGPIO(GPIOBank):
|
|
"""
|
|
Abstraction layer for the front panel GPIO
|
|
"""
|
|
EMIO_BASE = 54
|
|
FP_GPIO_OFFSET = 32 # Bit offset within the ps_gpio_* pins
|
|
|
|
def __init__(self, ddr):
|
|
GPIOBank.__init__(
|
|
self,
|
|
'zynq_gpio',
|
|
self.FP_GPIO_OFFSET + self.EMIO_BASE,
|
|
0xFFF, # use_mask
|
|
ddr
|
|
)
|
|
|
|
class BackpanelGPIO(GPIOBank):
|
|
"""
|
|
Abstraction layer for the back panel GPIO
|
|
"""
|
|
EMIO_BASE = 54
|
|
BP_GPIO_OFFSET = 45
|
|
LED_LINK = 0
|
|
LED_REF = 1
|
|
LED_GPS = 2
|
|
|
|
def __init__(self):
|
|
GPIOBank.__init__(
|
|
self,
|
|
'zynq_gpio',
|
|
self.BP_GPIO_OFFSET + self.EMIO_BASE,
|
|
0x7, # use_mask
|
|
0x7, # ddr
|
|
)
|
|
|
|
class MboardRegsControl(object):
|
|
"""
|
|
Control the FPGA Motherboard registers
|
|
"""
|
|
# Motherboard registers
|
|
M_COMPAT_NUM = 0x0000
|
|
MB_DATESTAMP = 0x0004
|
|
MB_GIT_HASH = 0x0008
|
|
MB_SCRATCH = 0x000C
|
|
MB_NUM_CE = 0x0010
|
|
MB_NUM_IO_CE = 0x0014
|
|
MB_CLOCK_CTRL = 0x0018
|
|
MB_XADC_RB = 0x001C
|
|
MB_BUS_CLK_RATE = 0x0020
|
|
MB_BUS_COUNTER = 0x0024
|
|
|
|
# Bitfield locations for the MB_CLOCK_CTRL register.
|
|
MB_CLOCK_CTRL_PPS_SEL_INT_10 = 0 # pps_sel is one-hot encoded!
|
|
MB_CLOCK_CTRL_PPS_SEL_INT_25 = 1
|
|
MB_CLOCK_CTRL_PPS_SEL_EXT = 2
|
|
MB_CLOCK_CTRL_PPS_SEL_GPSDO = 3
|
|
MB_CLOCK_CTRL_PPS_OUT_EN = 4 # output enabled = 1
|
|
MB_CLOCK_CTRL_MEAS_CLK_RESET = 12 # set to 1 to reset mmcm, default is 0
|
|
MB_CLOCK_CTRL_MEAS_CLK_LOCKED = 13 # locked indication for meas_clk mmcm
|
|
|
|
def __init__(self, label, log):
|
|
self.log = log
|
|
self.regs = UIO(
|
|
label=label,
|
|
read_only=False
|
|
)
|
|
self.poke32 = self.regs.poke32
|
|
self.peek32 = self.regs.peek32
|
|
|
|
def get_compat_number(self):
|
|
"""get FPGA compat number
|
|
|
|
This function reads back FPGA compat number.
|
|
The return is a tuple of
|
|
2 numbers: (major compat number, minor compat number )
|
|
"""
|
|
with self.regs.open():
|
|
compat_number = self.peek32(self.M_COMPAT_NUM)
|
|
minor = compat_number & 0xff
|
|
major = (compat_number>>16) & 0xff
|
|
return (major, minor)
|
|
|
|
def get_build_timestamp(self):
|
|
"""
|
|
Returns the build date/time for the FPGA image.
|
|
The return is datetime string with the ISO 8601 format
|
|
(YYYY-MM-DD HH:MM:SS.mmmmmm)
|
|
"""
|
|
with self.regs.open():
|
|
datestamp_rb = self.peek32(self.MB_DATESTAMP)
|
|
if datestamp_rb > 0:
|
|
dt_str = datetime.datetime(
|
|
year=((datestamp_rb>>17)&0x3F)+2000,
|
|
month=(datestamp_rb>>23)&0x0F,
|
|
day=(datestamp_rb>>27)&0x1F,
|
|
hour=(datestamp_rb>>12)&0x1F,
|
|
minute=(datestamp_rb>>6)&0x3F,
|
|
second=((datestamp_rb>>0)&0x3F))
|
|
self.log.trace("FPGA build timestamp: {}".format(str(dt_str)))
|
|
return str(dt_str)
|
|
else:
|
|
# Compatibility with FPGAs without datestamp capability
|
|
return ''
|
|
|
|
def get_git_hash(self):
|
|
"""
|
|
Returns the GIT hash for the FPGA build.
|
|
The return is a tuple of
|
|
2 numbers: (short git hash, bool: is the tree dirty?)
|
|
"""
|
|
with self.regs.open():
|
|
git_hash_rb = self.peek32(self.MB_GIT_HASH)
|
|
git_hash = git_hash_rb & 0x0FFFFFFF
|
|
tree_dirty = ((git_hash_rb & 0xF0000000) > 0)
|
|
dirtiness_qualifier = 'dirty' if tree_dirty else 'clean'
|
|
self.log.trace("FPGA build GIT Hash: {:07x} ({})".format(
|
|
git_hash, dirtiness_qualifier))
|
|
return (git_hash, dirtiness_qualifier)
|
|
|
|
def set_time_source(self, time_source, ref_clk_freq):
|
|
"""
|
|
Set time source
|
|
"""
|
|
pps_sel_val = 0x0
|
|
if time_source == 'internal':
|
|
assert ref_clk_freq in (10e6, 25e6)
|
|
if ref_clk_freq == 10e6:
|
|
self.log.trace("Setting time source to internal "
|
|
"(10 MHz reference)...")
|
|
pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_10
|
|
elif ref_clk_freq == 25e6:
|
|
self.log.trace("Setting time source to internal "
|
|
"(25 MHz reference)...")
|
|
pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_INT_25
|
|
elif time_source == 'external':
|
|
self.log.trace("Setting time source to external...")
|
|
pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_EXT
|
|
elif time_source == 'gpsdo':
|
|
self.log.trace("Setting time source to gpsdo...")
|
|
pps_sel_val = 0b1 << self.MB_CLOCK_CTRL_PPS_SEL_GPSDO
|
|
else:
|
|
assert False
|
|
with self.regs.open():
|
|
reg_val = self.peek32(self.MB_CLOCK_CTRL) & 0xFFFFFFF0
|
|
reg_val = reg_val | (pps_sel_val & 0xF)
|
|
self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
|
|
self.poke32(self.MB_CLOCK_CTRL, reg_val)
|
|
|
|
def enable_pps_out(self, enable):
|
|
"""
|
|
Enables the PPS/Trig output on the back panel
|
|
"""
|
|
self.log.trace("%s PPS/Trig output!",
|
|
"Enabling" if enable else "Disabling")
|
|
mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
|
|
with self.regs.open():
|
|
# mask the bit to clear it:
|
|
reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
|
|
if enable:
|
|
# set the bit if desired:
|
|
reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_PPS_OUT_EN)
|
|
self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
|
|
self.poke32(self.MB_CLOCK_CTRL, reg_val)
|
|
|
|
def reset_meas_clk_mmcm(self, reset=True):
|
|
"""
|
|
Reset or unreset the MMCM for the measurement clock in the FPGA TDC.
|
|
"""
|
|
self.log.trace("%s measurement clock MMCM reset...",
|
|
"Asserting" if reset else "Clearing")
|
|
mask = 0xFFFFFFFF ^ (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
|
|
with self.regs.open():
|
|
# mask the bit to clear it
|
|
reg_val = self.peek32(self.MB_CLOCK_CTRL) & mask
|
|
if reset:
|
|
# set the bit if desired
|
|
reg_val = reg_val | (0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_RESET)
|
|
self.log.trace("Writing MB_CLOCK_CTRL to 0x{:08X}".format(reg_val))
|
|
self.poke32(self.MB_CLOCK_CTRL, reg_val)
|
|
|
|
def get_meas_clock_mmcm_lock(self):
|
|
"""
|
|
Check the status of the MMCM for the measurement clock in the FPGA TDC.
|
|
"""
|
|
mask = 0b1 << self.MB_CLOCK_CTRL_MEAS_CLK_LOCKED
|
|
with self.regs.open():
|
|
reg_val = self.peek32(self.MB_CLOCK_CTRL)
|
|
locked = (reg_val & mask) > 0
|
|
if not locked:
|
|
self.log.warning("Measurement clock MMCM reporting unlocked. "
|
|
"MB_CLOCK_CTRL reg: 0x{:08X}".format(reg_val))
|
|
else:
|
|
self.log.trace("Measurement clock MMCM locked!")
|
|
return locked
|
|
|
|
|
|
###############################################################################
|
|
# Transport managers
|
|
###############################################################################
|
|
class N310XportMgrUDP(XportMgrUDP):
|
|
" N310-specific UDP configuration "
|
|
eth_tables = {'eth1': 'misc-enet-regs0', 'eth2': 'misc-enet-regs1'}
|
|
xbar_port_map = {'eth1': 0, 'eth2': 1}
|
|
xbar_dev = "/dev/crossbar0"
|
|
iface_config = {
|
|
'eth1': {
|
|
'label': 'misc-enet-regs0',
|
|
'xbar': 0,
|
|
'xbar_port': 0,
|
|
'ctrl_src_addr': 0,
|
|
},
|
|
'eth2': {
|
|
'label': 'misc-enet-regs1',
|
|
'xbar': 0,
|
|
'xbar_port': 1,
|
|
'ctrl_src_addr': 1,
|
|
},
|
|
}
|
|
|
|
class N310XportMgrLiberio(XportMgrLiberio):
|
|
" N310-specific Liberio configuration "
|
|
max_chan = 10
|
|
xbar_dev = "/dev/crossbar0"
|
|
xbar_port = 2
|
|
|
|
###############################################################################
|
|
# Main Class
|
|
###############################################################################
|
|
class n310(PeriphManagerBase):
|
|
"""
|
|
Holds N310 specific attributes and methods
|
|
"""
|
|
#########################################################################
|
|
# Overridables
|
|
#
|
|
# See PeriphManagerBase for documentation on these fields
|
|
#########################################################################
|
|
pids = [0x4242,]
|
|
mboard_eeprom_addr = "e0005000.i2c"
|
|
mboard_eeprom_max_len = 256
|
|
mboard_info = {"type": "n3xx",
|
|
"product": "n310"
|
|
}
|
|
mboard_max_rev = 4 # 4 == RevE
|
|
mboard_sensor_callback_map = {
|
|
'ref_locked': 'get_ref_lock_sensor',
|
|
'gps_locked': 'get_gps_lock_sensor',
|
|
'gps_time': 'get_gps_time_sensor',
|
|
'gps_tpv': 'get_gps_tpv_sensor',
|
|
'gps_sky': 'get_gps_sky_sensor',
|
|
'temp': 'get_temp_sensor',
|
|
'fan': 'get_fan_sensor',
|
|
}
|
|
dboard_eeprom_addr = "e0004000.i2c"
|
|
dboard_eeprom_max_len = 64
|
|
|
|
# We're on a Zynq target, so the following two come from the Zynq standard
|
|
# device tree overlay (tree/arch/arm/boot/dts/zynq-7000.dtsi)
|
|
dboard_spimaster_addrs = ["e0006000.spi", "e0007000.spi"]
|
|
# N310-specific settings
|
|
# Path to N310 FPGA bin file
|
|
# This file will always contain the current image, regardless of SFP type,
|
|
# dboard, etc. The host is responsible for providing a compatible image
|
|
# for the N310's current setup.
|
|
# Label for the mboard UIO
|
|
mboard_regs_label = "mboard-regs"
|
|
# Override the list of updateable components
|
|
updateable_components = {
|
|
'fpga': {
|
|
'callback': "update_fpga",
|
|
'path': '/lib/firmware/n3xx.bin',
|
|
'reset': True,
|
|
},
|
|
'dts': {
|
|
'callback': "update_dts",
|
|
'path': '/lib/firmware/n3xx.dts',
|
|
'output': '/lib/firmware/n3xx.dtbo',
|
|
'reset': False,
|
|
},
|
|
}
|
|
|
|
@staticmethod
|
|
def list_required_dt_overlays(eeprom_md, device_args):
|
|
"""
|
|
Lists device tree overlays that need to be applied before this class can
|
|
be used. List of strings.
|
|
Are applied in order.
|
|
|
|
eeprom_md -- Dictionary of info read out from the mboard EEPROM
|
|
device_args -- Arbitrary dictionary of info, typically user-defined
|
|
"""
|
|
return ['n3xx']
|
|
|
|
###########################################################################
|
|
# Ctor and device initialization tasks
|
|
###########################################################################
|
|
def __init__(self, args):
|
|
super(n310, self).__init__(args)
|
|
if not self._device_initialized:
|
|
# Don't try and figure out what's going on. Just give up.
|
|
return
|
|
self._tear_down = False
|
|
self._status_monitor_thread = None
|
|
self._ext_clock_freq = None
|
|
self._clock_source = None
|
|
self._time_source = None
|
|
self._available_endpoints = list(range(256))
|
|
self._bp_leds = None
|
|
try:
|
|
self._init_peripherals(args)
|
|
except Exception as ex:
|
|
self.log.error("Failed to initialize motherboard: %s", str(ex))
|
|
self._initialization_status = str(ex)
|
|
self._device_initialized = False
|
|
|
|
def _check_fpga_compat(self):
|
|
" Throw an exception if the compat numbers don't match up "
|
|
actual_compat = self.mboard_regs_control.get_compat_number()
|
|
self.log.debug("Actual FPGA compat number: {:d}.{:d}".format(
|
|
actual_compat[0], actual_compat[1]
|
|
))
|
|
assert_compat_number(
|
|
N3XX_FPGA_COMPAT,
|
|
self.mboard_regs_control.get_compat_number(),
|
|
component="FPGA",
|
|
fail_on_old_minor=True,
|
|
log=self.log
|
|
)
|
|
|
|
def _init_ref_clock_and_time(self, default_args):
|
|
"""
|
|
Initialize clock and time sources. After this function returns, the
|
|
reference signals going to the FPGA are valid.
|
|
"""
|
|
self._ext_clock_freq = float(
|
|
default_args.get('ext_clock_freq', N3XX_DEFAULT_EXT_CLOCK_FREQ)
|
|
)
|
|
if len(self.dboards) == 0:
|
|
self.log.warning(
|
|
"No dboards found, skipping setting clock and time source " \
|
|
"configuration."
|
|
)
|
|
self._clock_source = N3XX_DEFAULT_CLOCK_SOURCE
|
|
self._time_source = N3XX_DEFAULT_TIME_SOURCE
|
|
else:
|
|
self.set_clock_source(
|
|
default_args.get('clock_source', N3XX_DEFAULT_CLOCK_SOURCE)
|
|
)
|
|
self.set_time_source(
|
|
default_args.get('time_source', N3XX_DEFAULT_TIME_SOURCE)
|
|
)
|
|
self.enable_pps_out(
|
|
default_args.get('pps_export', True)
|
|
)
|
|
|
|
def _init_meas_clock(self):
|
|
"""
|
|
Initialize the TDC measurement clock. After this function returns, the
|
|
FPGA TDC meas_clock is valid.
|
|
"""
|
|
# No need to toggle reset here, simply confirm it is out of reset.
|
|
self.mboard_regs_control.reset_meas_clk_mmcm(False)
|
|
if not self.mboard_regs_control.get_meas_clock_mmcm_lock():
|
|
raise RuntimeError("Measurement clock failed to init")
|
|
|
|
def _monitor_status(self):
|
|
"""
|
|
Status monitoring thread: This should be executed in a thread. It will
|
|
continuously monitor status of the following peripherals:
|
|
|
|
- GPS lock (update back-panel GPS LED)
|
|
- REF lock (update back-panel REF LED)
|
|
"""
|
|
self.log.trace("Launching monitor thread...")
|
|
cond = threading.Condition()
|
|
cond.acquire()
|
|
while not self._tear_down:
|
|
gps_locked = bool(self._gpios.get("GPS-LOCKOK"))
|
|
self.log.trace("Setting GPS LED to {}".format(gps_locked))
|
|
self._bp_leds.set(self._bp_leds.LED_GPS, int(gps_locked))
|
|
ref_locked = self.get_ref_lock_sensor()['value'] == 'true'
|
|
self.log.trace("Setting REF LED to {}".format(ref_locked))
|
|
self._bp_leds.set(self._bp_leds.LED_REF, int(ref_locked))
|
|
# Now wait
|
|
if cond.wait_for(
|
|
lambda: self._tear_down,
|
|
N3XX_MONITOR_THREAD_INTERVAL):
|
|
break
|
|
cond.release()
|
|
self.log.trace("Terminating monitor thread.")
|
|
|
|
def _init_peripherals(self, args):
|
|
"""
|
|
Turn on all peripherals. This may throw an error on failure, so make
|
|
sure to catch it.
|
|
|
|
Periphals are initialized in the order of least likely to fail, to most
|
|
likely.
|
|
"""
|
|
# Init peripherals
|
|
self.log.trace("Initializing TCA6424 port expander controls...")
|
|
self._gpios = TCA6424(int(self.mboard_info['rev']))
|
|
self.log.trace("Initializing back panel led controls...")
|
|
self._bp_leds = BackpanelGPIO()
|
|
self.log.trace("Enabling power of MGT156MHZ clk")
|
|
self._gpios.set("PWREN-CLK-MGT156MHz")
|
|
self.enable_1g_ref_clock()
|
|
self.enable_gps(
|
|
enable=bool(
|
|
args.default_args.get('enable_gps', N3XX_DEFAULT_ENABLE_GPS)
|
|
)
|
|
)
|
|
self.enable_fp_gpio(
|
|
enable=bool(
|
|
args.default_args.get(
|
|
'enable_fp_gpio',
|
|
N3XX_DEFAULT_ENABLE_FPGPIO
|
|
)
|
|
)
|
|
)
|
|
# Init Mboard Regs
|
|
self.mboard_regs_control = MboardRegsControl(
|
|
self.mboard_regs_label, self.log)
|
|
self.mboard_regs_control.get_git_hash()
|
|
self.mboard_regs_control.get_build_timestamp()
|
|
self._check_fpga_compat()
|
|
# Init clocking
|
|
self.enable_ref_clock(enable=True)
|
|
self._ext_clock_freq = None
|
|
self._init_ref_clock_and_time(args.default_args)
|
|
self._init_meas_clock()
|
|
# Init CHDR transports
|
|
self._xport_mgrs = {
|
|
'udp': N310XportMgrUDP(self.log.getChild('UDP')),
|
|
'liberio': N310XportMgrLiberio(self.log.getChild('liberio')),
|
|
}
|
|
# Spawn status monitoring thread
|
|
self.log.trace("Spawning status monitor thread...")
|
|
self._status_monitor_thread = threading.Thread(
|
|
target=self._monitor_status,
|
|
name="N3xxStatusMonitorThread",
|
|
daemon=True,
|
|
)
|
|
self._status_monitor_thread.start()
|
|
# Init complete.
|
|
self.log.info("mboard info: {}".format(self.mboard_info))
|
|
|
|
###########################################################################
|
|
# Session init and deinit
|
|
###########################################################################
|
|
def init(self, args):
|
|
"""
|
|
Calls init() on the parent class, and then programs the Ethernet
|
|
dispatchers accordingly.
|
|
"""
|
|
if not self._device_initialized:
|
|
self.log.warning(
|
|
"Cannot run init(), device was never fully initialized!")
|
|
return False
|
|
if args.get("clock_source", "") != "":
|
|
self.set_clock_source(args.get("clock_source"))
|
|
if args.get("time_source", "") != "":
|
|
self.set_time_source(args.get("time_source"))
|
|
result = super(n310, self).init(args)
|
|
for xport_mgr in itervalues(self._xport_mgrs):
|
|
xport_mgr.init(args)
|
|
return result
|
|
|
|
def deinit(self):
|
|
"""
|
|
Clean up after a UHD session terminates.
|
|
"""
|
|
if not self._device_initialized:
|
|
self.log.warning(
|
|
"Cannot run deinit(), device was never fully initialized!")
|
|
return
|
|
super(n310, self).deinit()
|
|
for xport_mgr in itervalues(self._xport_mgrs):
|
|
xport_mgr.deinit()
|
|
self.log.trace("Resetting SID pool...")
|
|
self._available_endpoints = list(range(256))
|
|
|
|
def tear_down(self):
|
|
"""
|
|
Tear down all members that need to be specially handled before
|
|
deconstruction.
|
|
For N310, this means the overlay.
|
|
"""
|
|
self.log.trace("Tearing down N3xx device...")
|
|
self._tear_down = True
|
|
if self._device_initialized:
|
|
self._status_monitor_thread.join(3 * N3XX_MONITOR_THREAD_INTERVAL)
|
|
if self._status_monitor_thread.is_alive():
|
|
self.log.error("Could not terminate monitor thread!")
|
|
active_overlays = self.list_active_overlays()
|
|
self.log.trace("N310 has active device tree overlays: {}".format(
|
|
active_overlays
|
|
))
|
|
for overlay in active_overlays:
|
|
dtoverlay.rm_overlay(overlay)
|
|
|
|
###########################################################################
|
|
# Transport API
|
|
###########################################################################
|
|
def request_xport(
|
|
self,
|
|
dst_address,
|
|
suggested_src_address,
|
|
xport_type
|
|
):
|
|
"""
|
|
See PeriphManagerBase.request_xport() for docs.
|
|
"""
|
|
# Try suggested address first, then just pick the first available one:
|
|
src_address = suggested_src_address
|
|
if src_address not in self._available_endpoints:
|
|
if len(self._available_endpoints) == 0:
|
|
raise RuntimeError(
|
|
"Depleted pool of SID endpoints for this device!")
|
|
else:
|
|
src_address = self._available_endpoints[0]
|
|
sid = SID(src_address << 16 | dst_address)
|
|
# Note: This SID may change its source address!
|
|
self.log.debug(
|
|
"request_xport(dst=0x%04X, suggested_src_address=0x%04X, xport_type=%s): " \
|
|
"operating on temporary SID: %s",
|
|
dst_address, suggested_src_address, str(xport_type), str(sid))
|
|
# FIXME token!
|
|
assert self.mboard_info['rpc_connection'] in ('remote', 'local')
|
|
if self.mboard_info['rpc_connection'] == 'remote':
|
|
return self._xport_mgrs['udp'].request_xport(
|
|
sid,
|
|
xport_type,
|
|
)
|
|
elif self.mboard_info['rpc_connection'] == 'local':
|
|
return self._xport_mgrs['liberio'].request_xport(
|
|
sid,
|
|
xport_type,
|
|
)
|
|
|
|
def commit_xport(self, xport_info):
|
|
"""
|
|
See PeriphManagerBase.commit_xport() for docs.
|
|
|
|
Reminder: All connections are incoming, i.e. "send" or "TX" means
|
|
remote device to local device, and "receive" or "RX" means this local
|
|
device to remote device. "Remote device" can be, for example, a UHD
|
|
session.
|
|
"""
|
|
## Go, go, go
|
|
assert self.mboard_info['rpc_connection'] in ('remote', 'local')
|
|
sid = SID(xport_info['send_sid'])
|
|
self._available_endpoints.remove(sid.src_ep)
|
|
self.log.debug("Committing transport for SID %s, xport info: %s",
|
|
str(sid), str(xport_info))
|
|
if self.mboard_info['rpc_connection'] == 'remote':
|
|
return self._xport_mgrs['udp'].commit_xport(sid, xport_info)
|
|
elif self.mboard_info['rpc_connection'] == 'local':
|
|
return self._xport_mgrs['liberio'].commit_xport(sid, xport_info)
|
|
|
|
###########################################################################
|
|
# Clock/Time API
|
|
###########################################################################
|
|
def get_clock_sources(self):
|
|
" Lists all available clock sources. "
|
|
self.log.trace("Listing available clock sources...")
|
|
return ('external', 'internal', 'gpsdo')
|
|
|
|
def get_clock_source(self):
|
|
" Returns the currently selected clock source "
|
|
return self._clock_source
|
|
|
|
def set_clock_source(self, *args):
|
|
"""
|
|
Switch reference clock.
|
|
|
|
Throws if clock_source is not a valid value.
|
|
"""
|
|
clock_source = args[0]
|
|
assert clock_source in self.get_clock_sources()
|
|
self.log.trace("Setting clock source to `{}'".format(clock_source))
|
|
if clock_source == self.get_clock_source():
|
|
self.log.trace("Nothing to do -- clock source already set.")
|
|
return
|
|
if clock_source == 'internal':
|
|
self._gpios.set("CLK-MAINSEL-EX_B")
|
|
self._gpios.set("CLK-MAINSEL-25MHz")
|
|
self._gpios.reset("CLK-MAINSEL-GPS")
|
|
elif clock_source == 'gpsdo':
|
|
self._gpios.set("CLK-MAINSEL-EX_B")
|
|
self._gpios.reset("CLK-MAINSEL-25MHz")
|
|
self._gpios.set("CLK-MAINSEL-GPS")
|
|
else: # external
|
|
self._gpios.reset("CLK-MAINSEL-EX_B")
|
|
self._gpios.reset("CLK-MAINSEL-GPS")
|
|
# SKY13350 needs to be in known state
|
|
self._gpios.set("CLK-MAINSEL-25MHz")
|
|
self._clock_source = clock_source
|
|
ref_clk_freq = self.get_ref_clock_freq()
|
|
self.log.info("Reference clock frequency is: {} MHz".format(
|
|
ref_clk_freq/1e6
|
|
))
|
|
for slot, dboard in enumerate(self.dboards):
|
|
if hasattr(dboard, 'update_ref_clock_freq'):
|
|
self.log.trace(
|
|
"Updating reference clock on dboard %d to %f MHz...",
|
|
slot, ref_clk_freq/1e6
|
|
)
|
|
dboard.update_ref_clock_freq(ref_clk_freq)
|
|
|
|
def set_ref_clock_freq(self, freq):
|
|
"""
|
|
Tell our USRP what the frequency of the external reference clock is.
|
|
|
|
Will throw if it's not a valid value.
|
|
"""
|
|
assert freq in (10e6, 20e6, 25e6)
|
|
self.log.debug("We've been told the external reference clock " \
|
|
"frequency is {} MHz.".format(freq/1e6))
|
|
if self._ext_clk_freq == freq:
|
|
self.log.trace("New external reference clock frequency " \
|
|
"assignment matches previous assignment. Ignoring " \
|
|
"update command.")
|
|
return
|
|
self._ext_clock_freq = freq
|
|
if self.get_clock_source() == 'external':
|
|
for slot, dboard in enumerate(self.dboards):
|
|
if hasattr(dboard, 'update_ref_clock_freq'):
|
|
self.log.trace(
|
|
"Updating reference clock on dboard %d to %f MHz...",
|
|
slot, freq/1e6
|
|
)
|
|
dboard.update_ref_clock_freq(freq)
|
|
|
|
def get_ref_clock_freq(self):
|
|
" Returns the currently active reference clock frequency"
|
|
return {
|
|
'internal': 25e6,
|
|
'external': self._ext_clock_freq,
|
|
'gpsdo': 20e6,
|
|
}[self._clock_source]
|
|
|
|
def get_time_sources(self):
|
|
" Returns list of valid time sources "
|
|
return ['internal', 'external', 'gpsdo']
|
|
|
|
def get_time_source(self):
|
|
" Return the currently selected time source "
|
|
return self._time_source
|
|
|
|
def set_time_source(self, time_source):
|
|
" Set a time source "
|
|
assert time_source in self.get_time_sources()
|
|
if time_source == self.get_time_source():
|
|
self.log.trace("Nothing to do -- time source already set.")
|
|
return
|
|
self._time_source = time_source
|
|
self.mboard_regs_control.set_time_source(
|
|
time_source, self.get_ref_clock_freq())
|
|
|
|
###########################################################################
|
|
# Hardware periphal controls
|
|
###########################################################################
|
|
def enable_pps_out(self, enable):
|
|
" Export a PPS/Trigger to the back panel "
|
|
self.mboard_regs_control.enable_pps_out(enable)
|
|
|
|
def enable_gps(self, enable):
|
|
"""
|
|
Turn power to the GPS off or on.
|
|
"""
|
|
self.log.trace("{} power to GPS".format(
|
|
"Enabling" if enable else "Disabling"
|
|
))
|
|
self._gpios.set("PWREN-GPS", int(bool(enable)))
|
|
|
|
def enable_fp_gpio(self, enable):
|
|
"""
|
|
Turn power to the front panel GPIO off or on.
|
|
"""
|
|
self.log.trace("{} power to front-panel GPIO".format(
|
|
"Enabling" if enable else "Disabling"
|
|
))
|
|
self._gpios.set("FPGA-GPIO-EN", int(bool(enable)))
|
|
|
|
def enable_ref_clock(self, enable):
|
|
"""
|
|
Enables the ref clock voltage (+3.3-MAINREF). Without setting this to
|
|
True, *no* ref clock works.
|
|
"""
|
|
self.log.trace("{} power to reference clocks".format(
|
|
"Enabling" if enable else "Disabling"
|
|
))
|
|
self._gpios.set("PWREN-CLK-MAINREF", int(bool(enable)))
|
|
|
|
def enable_1g_ref_clock(self):
|
|
"""
|
|
Enables 125 MHz refclock for 1G interface.
|
|
"""
|
|
self.log.trace("Enable 125 MHz Clock for 1G SFP interface.")
|
|
self._gpios.set("NETCLK-CE")
|
|
self._gpios.set("NETCLK-RESETn", 0)
|
|
self._gpios.set("NETCLK-PR0", 1)
|
|
self._gpios.set("NETCLK-PR1", 1)
|
|
self._gpios.set("NETCLK-OD0", 1)
|
|
self._gpios.set("NETCLK-OD1", 1)
|
|
self._gpios.set("NETCLK-OD2", 0)
|
|
self._gpios.set("PWREN-CLK-WB-25MHz", 1)
|
|
self.log.trace("Finished configuring NETCLK CDCM.")
|
|
self._gpios.set("NETCLK-RESETn", 1)
|
|
|
|
###########################################################################
|
|
# Sensors
|
|
###########################################################################
|
|
def get_ref_lock_sensor(self):
|
|
"""
|
|
The N310 has no ref lock sensor, but because the ref lock is
|
|
historically considered a motherboard-level sensor, we will return the
|
|
combined lock status of all daughterboards. If no dboard is connected,
|
|
or none has a ref lock sensor, we simply return True.
|
|
"""
|
|
self.log.trace(
|
|
"Querying ref lock status from %d dboards.",
|
|
len(self.dboards)
|
|
)
|
|
lock_status = all([
|
|
not hasattr(db, 'get_ref_lock') or db.get_ref_lock()
|
|
for db in self.dboards
|
|
])
|
|
return {
|
|
'name': 'ref_locked',
|
|
'type': 'BOOLEAN',
|
|
'unit': 'locked' if lock_status else 'unlocked',
|
|
'value': str(lock_status).lower(),
|
|
}
|
|
|
|
def get_temp_sensor(self):
|
|
"""
|
|
Get temperature sensor reading of the N3xx.
|
|
"""
|
|
self.log.trace("Reading fpga temperature.")
|
|
return_val = '-1'
|
|
try:
|
|
raw_val = read_thermal_sensor_value('fpga-thermal-zone', 'temp')
|
|
return_val = str(raw_val/1000)
|
|
except ValueError:
|
|
self.log.warning("Error when converting temperature value")
|
|
except KeyError:
|
|
self.log.warning("Can't read temp on fpga-thermal-zone")
|
|
return {
|
|
'name': 'temperature',
|
|
'type': 'REALNUM',
|
|
'unit': 'C',
|
|
'value': return_val
|
|
}
|
|
|
|
def get_fan_sensor(self):
|
|
"""
|
|
Get cooling device reading of N3xx. In this case the speed of fan 0.
|
|
"""
|
|
self.log.trace("Reading fpga cooling device.")
|
|
return_val = '-1'
|
|
try:
|
|
raw_val = read_thermal_sensor_value('ec-fan0', 'cur_state')
|
|
return_val = str(raw_val)
|
|
except ValueError:
|
|
self.log.warning("Error when converting fan speed value")
|
|
except KeyError:
|
|
self.log.warning("Can't read cur_state on ec-fan0")
|
|
return {
|
|
'name': 'cooling fan',
|
|
'type': 'INTEGER',
|
|
'unit': 'rpm',
|
|
'value': return_val
|
|
}
|
|
|
|
def get_gps_lock_sensor(self):
|
|
"""
|
|
Get lock status of GPS as a sensor dict
|
|
"""
|
|
self.log.trace("Reading status GPS lock pin from port expander")
|
|
gps_locked = bool(self._gpios.get("GPS-LOCKOK"))
|
|
return {
|
|
'name': 'gps_lock',
|
|
'type': 'BOOLEAN',
|
|
'unit': 'locked' if gps_locked else 'unlocked',
|
|
'value': str(gps_locked).lower(),
|
|
}
|
|
|
|
def get_gps_time_sensor(self):
|
|
"""
|
|
Calculates GPS time using a TPV response from GPSd, and returns as a sensor dict
|
|
|
|
This time is not high accuracy.
|
|
"""
|
|
self.log.trace("Polling GPS time results from GPSD")
|
|
with GPSDIface() as gps_iface:
|
|
response_mode = 0
|
|
# Read responses from GPSD until we get a non-trivial mode
|
|
while response_mode <= 0:
|
|
gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15)
|
|
self.log.trace("GPS info: {}".format(gps_info))
|
|
response_mode = gps_info.get("mode", 0)
|
|
time_str = gps_info.get("time", "")
|
|
self.log.trace("GPS time string: {}".format(time_str))
|
|
time_dt = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
self.log.trace("GPS datetime: {}".format(time_dt))
|
|
epoch_dt = datetime.datetime(1970, 1, 1)
|
|
gps_time = int((time_dt - epoch_dt).total_seconds())
|
|
return {
|
|
'name': 'gps_time',
|
|
'type': 'INTEGER',
|
|
'unit': 'seconds',
|
|
'value': str(gps_time),
|
|
}
|
|
|
|
def get_gps_tpv_sensor(self):
|
|
"""
|
|
Get a TPV response from GPSd as a sensor dict
|
|
"""
|
|
self.log.trace("Polling GPS TPV results from GPSD")
|
|
with GPSDIface() as gps_iface:
|
|
response_mode = 0
|
|
# Read responses from GPSD until we get a non-trivial mode
|
|
while response_mode <= 0:
|
|
gps_info = gps_iface.get_gps_info(resp_class='tpv', timeout=15)
|
|
self.log.trace("GPS info: {}".format(gps_info))
|
|
response_mode = gps_info.get("mode", 0)
|
|
# Return the JSON'd results
|
|
gps_tpv = json.dumps(gps_info)
|
|
return {
|
|
'name': 'gps_tpv',
|
|
'type': 'STRING',
|
|
'unit': '',
|
|
'value': gps_tpv,
|
|
}
|
|
|
|
def get_gps_sky_sensor(self):
|
|
"""
|
|
Get a SKY response from GPSd as a sensor dict
|
|
"""
|
|
self.log.trace("Polling GPS SKY results from GPSD")
|
|
with GPSDIface() as gps_iface:
|
|
# Just get the first SKY result
|
|
gps_info = gps_iface.get_gps_info(resp_class='sky', timeout=15)
|
|
# Return the JSON'd results
|
|
gps_sky = json.dumps(gps_info)
|
|
return {
|
|
'name': 'gps_sky',
|
|
'type': 'STRING',
|
|
'unit': '',
|
|
'value': gps_sky,
|
|
}
|
|
|
|
###########################################################################
|
|
# EEPROMs
|
|
###########################################################################
|
|
def get_mb_eeprom(self):
|
|
"""
|
|
Return a dictionary with EEPROM contents.
|
|
|
|
All key/value pairs are string -> string.
|
|
|
|
We don't actually return the EEPROM contents, instead, we return the
|
|
mboard info again. This filters the EEPROM contents to what we think
|
|
the user wants to know/see.
|
|
"""
|
|
return self.mboard_info
|
|
|
|
def set_mb_eeprom(self, eeprom_vals):
|
|
"""
|
|
See PeriphManagerBase.set_mb_eeprom() for docs.
|
|
"""
|
|
self.log.warn("Called set_mb_eeprom(), but not implemented!")
|
|
raise NotImplementedError
|
|
|
|
def get_db_eeprom(self, dboard_idx):
|
|
"""
|
|
See PeriphManagerBase.get_db_eeprom() for docs.
|
|
"""
|
|
try:
|
|
dboard = self.dboards[dboard_idx]
|
|
except KeyError:
|
|
error_msg = "Attempted to access invalid dboard index `{}' " \
|
|
"in get_db_eeprom()!".format(dboard_idx)
|
|
self.log.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
db_eeprom_data = copy.copy(dboard.device_info)
|
|
if hasattr(dboard, 'get_user_eeprom_data') and \
|
|
callable(dboard.get_user_eeprom_data):
|
|
for blob_id, blob in iteritems(dboard.get_user_eeprom_data()):
|
|
if blob_id in db_eeprom_data:
|
|
self.log.warn("EEPROM user data contains invalid blob ID " \
|
|
"%s", blob_id)
|
|
else:
|
|
db_eeprom_data[blob_id] = blob
|
|
return db_eeprom_data
|
|
|
|
def set_db_eeprom(self, dboard_idx, eeprom_data):
|
|
"""
|
|
Write new EEPROM contents with eeprom_map.
|
|
|
|
Arguments:
|
|
dboard_idx -- Slot index of dboard
|
|
eeprom_data -- Dictionary of EEPROM data to be written. It's up to the
|
|
specific device implementation on how to handle it.
|
|
"""
|
|
try:
|
|
dboard = self.dboards[dboard_idx]
|
|
except KeyError:
|
|
error_msg = "Attempted to access invalid dboard index `{}' " \
|
|
"in set_db_eeprom()!".format(dboard_idx)
|
|
self.log.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
if not hasattr(dboard, 'set_user_eeprom_data') or \
|
|
not callable(dboard.set_user_eeprom_data):
|
|
error_msg = "Dboard has no set_user_eeprom_data() method!"
|
|
self.log.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
safe_db_eeprom_user_data = {}
|
|
for blob_id, blob in iteritems(eeprom_data):
|
|
if blob_id in dboard.device_info:
|
|
error_msg = "Trying to overwrite read-only EEPROM " \
|
|
"entry `{}'!".format(blob_id)
|
|
self.log.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
if not isinstance(blob, str) and not isinstance(blob, bytes):
|
|
error_msg = "Blob data for ID `{}' is not a " \
|
|
"string!".format(blob_id)
|
|
self.log.error(error_msg)
|
|
raise RuntimeError(error_msg)
|
|
assert isinstance(blob, str)
|
|
safe_db_eeprom_user_data[blob_id] = blob.encode('ascii')
|
|
dboard.set_user_eeprom_data(safe_db_eeprom_user_data)
|
|
|
|
###########################################################################
|
|
# Component updating
|
|
###########################################################################
|
|
@no_rpc
|
|
def update_fpga(self, filepath, metadata):
|
|
"""
|
|
Update the FPGA image in the filesystem and reload the overlay
|
|
:param filepath: path to new FPGA image
|
|
:param metadata: Dictionary of strings containing metadata
|
|
"""
|
|
self.log.trace("Updating FPGA with image at {} (metadata: `{}')"
|
|
.format(filepath, str(metadata)))
|
|
_, file_extension = os.path.splitext(filepath)
|
|
# Cut off the period from the file extension
|
|
file_extension = file_extension[1:].lower()
|
|
binfile_path = self.updateable_components['fpga']['path']
|
|
if file_extension == "bit":
|
|
self.log.trace("Converting bit to bin file and writing to {}"
|
|
.format(binfile_path))
|
|
from usrp_mpm.fpga_bit_to_bin import fpga_bit_to_bin
|
|
fpga_bit_to_bin(filepath, binfile_path, flip=True)
|
|
elif file_extension == "bin":
|
|
self.log.trace("Copying bin file to {}"
|
|
.format(binfile_path))
|
|
shutil.copy(filepath, binfile_path)
|
|
else:
|
|
self.log.error("Invalid FPGA bitfile: {}"
|
|
.format(filepath))
|
|
raise RuntimeError("Invalid N310 FPGA bitfile")
|
|
# RPC server will reload the periph manager after this.
|
|
return True
|
|
|
|
@no_rpc
|
|
def update_dts(self, filepath, metadata):
|
|
"""
|
|
Update the DTS image in the filesystem
|
|
:param filepath: path to new DTS image
|
|
:param metadata: Dictionary of strings containing metadata
|
|
"""
|
|
dtsfile_path = self.updateable_components['dts']['path']
|
|
self.log.trace("Updating DTS with image at %s to %s (metadata: %s)",
|
|
filepath, dtsfile_path, str(metadata))
|
|
shutil.copy(filepath, dtsfile_path)
|
|
dtbofile_path = self.updateable_components['dts']['output']
|
|
self.log.trace("Compiling to %s...", dtbofile_path)
|
|
dtc_command = [
|
|
'dtc',
|
|
'--symbols',
|
|
'-O', 'dtb',
|
|
'-q', # Suppress warnings
|
|
'-o',
|
|
dtbofile_path,
|
|
dtsfile_path,
|
|
]
|
|
self.log.trace("Executing command: `$ %s'", " ".join(dtc_command))
|
|
try:
|
|
out = subprocess.check_output(dtc_command)
|
|
if out.strip() != "":
|
|
self.log.debug("`dtc' command output: \n%s", out)
|
|
except OSError as ex:
|
|
self.log.error("Could not execute `dtc' command. Binary probably "\
|
|
"not installed. Please compile DTS by hand.")
|
|
# No fatal error here, in order not to break the current workflow
|
|
except subprocess.CalledProcessError as ex:
|
|
self.log.error("Error executing `dtc': %s", str(ex))
|
|
return False
|
|
return True
|
|
|
|
#######################################################################
|
|
# Claimer API
|
|
#######################################################################
|
|
def claim(self):
|
|
"""
|
|
This is called when the device is claimed, in case the device needs to
|
|
run any actions on claiming (e.g., light up an LED).
|
|
"""
|
|
if self._bp_leds is not None:
|
|
# Light up LINK
|
|
self._bp_leds.set(self._bp_leds.LED_LINK, 1)
|
|
|
|
def unclaim(self):
|
|
"""
|
|
This is called when the device is unclaimed, in case the device needs
|
|
to run any actions on claiming (e.g., turn off an LED).
|
|
"""
|
|
if self._bp_leds is not None:
|
|
# Turn off LINK
|
|
self._bp_leds.set(self._bp_leds.LED_LINK, 0)
|
|
|