#!/usr/bin/env python
#
# Copyright 2017 Ettus Research (National Instruments)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
"""
N310 Built-In Self Test (BIST)
"""
from __future__ import print_function
import sys
import socket
import json
from datetime import datetime
import argparse
import pyudev
def post_results(results):
"""
Given a dictionary, post the results.
This will print the results as JSON to stdout.
"""
print(json.dumps(
results,
sort_keys=True,
indent=4,
separators=(',', ': ')
))
class N310BIST(object):
"""
BIST Tool for the USRP N3xx series
"""
# This defines special tests that are really collections of other tests.
collections = {
'standard': ["ddr3", "gpsdo", "rtc", "temp", "clock_int", "tpm"],
'extended': "*",
}
@staticmethod
def make_arg_parser():
"""
Return arg parser
"""
parser = argparse.ArgumentParser(
description="N3xx BIST Tool",
)
parser.add_argument(
'-n', '--dry-run', action='store_true',
help="Fake out the tests. All tests will return a valid response," \
" but will not actually interact with hardware.",
)
parser.add_argument(
'tests',
help="List the tests that should be run",
nargs='+', # There has to be at least one
)
return parser
def __init__(self):
self.args = N310BIST.make_arg_parser().parse_args()
self.tests_to_run = set()
for test in self.args.tests:
if test in self.collections:
for test in self.expand_collection(test):
self.tests_to_run.add(test)
else:
self.tests_to_run.add(test)
def expand_collection(self, coll):
"""
Return names of tests in a collection
"""
tests = self.collections[coll]
if tests == "*":
tests = {x.replace('bist_', '')
for x in dir(self)
if x.find('bist_') == 0
}
else:
tests = set(tests)
return tests
def run(self):
"""
Execute tests.
Returns True on Success.
"""
def execute_test(testname):
"""
Actually run a test.
"""
testmethod_name = "bist_{0}".format(testname)
sys.stderr.write(
"Executing test method: {0}\n\n".format(testmethod_name)
)
try:
return getattr(self, testmethod_name)()
except AttributeError:
sys.stderr.write("Test not defined: {}\n".format(testname))
return False, {}
tests_successful = True
result = {}
for test in self.tests_to_run:
status, result_data = execute_test(test)
tests_successful = tests_successful and status
if tests_successful:
result[test] = result_data
post_results(result)
return tests_successful
#############################################################################
# BISTS
# All bist_* methods must return True/False success values!
#############################################################################
def bist_rtc(self):
"""
BIST for RTC (real time clock)
Return dictionary:
- time: Returns the current UTC time, with seconds-accuracy, in ISO 8601
format, as a string.
Return status:
Unless the 'date' command fails (which is used under the hood), will
always return True.
"""
assert 'rtc' in self.tests_to_run
result = {}
try:
utc_time = datetime.utcnow().replace(microseconds=0).isoformat()
result['time'] = utc_time + "+00:00"
except:
if self.args.dry_run:
result['time'] = '2017-07-11T17:31:03+00:00'
else:
return False, {}
return True, result
def bist_ddr3(self):
"""
BIST for PL DDR3 DRAM
Description: Calls a test to examine the speed of the DDR3
External Equipment: None
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'speed': Return numeric value of speed in MB/s
}
"""
assert 'ddr3' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_gpsdo(self):
"""
BIST for GPSDO
Description: Returns GPS information
External Equipment: None; Recommend attaching an antenna or providing
fake GPS information
Return dictionary: A TPV dictionary as returned by gpsd.
See also: http://www.catb.org/gpsd/gpsd_json.html
Check for mode 2 or 3 to see if it's locked.
"""
assert 'gpsdo' in self.tests_to_run
if self.args.dry_run:
return True, {
"class": "TPV",
"time": "2017-04-30T11:48:20.10Z",
"ept": 0.005,
"lat": 30.407899,
"lon": -97.726634,
"alt": 1327.689,
"epx": 15.319,
"epy": 17.054,
"epv": 124.484,
"track": 10.3797,
"speed": 0.091,
"climb": -0.085,
"eps": 34.11,
"mode": 3
}
my_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
my_sock.connect(('localhost', 2947))
sys.stderr.write("Connected to GPSDO socket.\n")
query_cmd = b'?WATCH={"enable":true,"json":true}'
my_sock.sendall(query_cmd)
sys.stderr.write("Sent query: {}\n".format(query_cmd))
while my_sock.recv(1) != b'\n':
pass
sys.stderr.write("Received initial newline.\n")
result = {}
while result.get('class', None) != 'TPV':
json_result = b''
next_char = b''
while next_char != b'\n':
json_result += next_char
next_char = my_sock.recv(1)
sys.stderr.write(
"Received JSON response: {}\n\n".format(json_result)
)
result = json.loads(json_result.decode('ascii'))
my_sock.sendall(b'?WATCH={"enable":false}')
my_sock.close()
return True, result
def bist_tpm(self):
"""
BIST for TPM (Trusted Platform Module)
Description:!!! I don't know how this will be tested !!!
External Equipment: None
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'idk': Returns some stuff??
}
"""
assert 'tpm' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_clock_int(self):
"""
BIST for clock lock from internal source
Description: Checks to see if the N3xx can lock to the internal reference clock
External Equipment: None
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'locked': Return TRUE if able to lock to internal reference, else FALSE
}
"""
assert 'clock_int' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_clock_ext(self):
"""
BIST for clock lock from external source
Description: Checks to see if the N3xx can lock to the external reference clock
External Equipment: Reference Source outputted into the "Ref In"
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'locked': Return TRUE if able to lock to external reference, else FALSE
}
"""
assert 'clock_ext' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_usbhost(self):
"""
BIST for USB host functionality
Description:!!! I'm considering removing this test !!!
External Equipment: USB device attached to USB hOST
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'devices': [] Returns all devices seen on USB hub
}
"""
assert 'usbhost' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_sfp(self):
"""
BIST for SFP+ ports:
Description: Uses one SFP+ port to test the other. Returns the speeds
External Equipment: External loopback cable between SFP+ port 0 and 1 is required
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'speed01': Returns speed in Gbit/s from port 0 to port 1
'speed10': Returns speed in Gbit/s from port 1 to port 0
}
"""
assert 'sfp' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_gpio(self):
"""
BIST for GPIO
Description: Writes and reads the values to the GPIO
Needed Equipment: External loopback as follows
GPIO
0<->6
1<->7
2<->8
3<->9
4<->10
5<->11
JSON{
'status': Return TRUE if no errors occurred, else FALSE
'expected': Return expected read values
'returned': Return read values
}
"""
assert 'gpio' in self.tests_to_run
sys.stderr.write("Test not implemented.\n")
return True, {}
def bist_temp(self):
"""
BIST for temperature sensors
Description: Reads the temperature sensors on the motherboards and
returns their values in mC
Return dictionary:
- : temp in mC
"""
assert 'temp' in self.tests_to_run
if self.args.dry_run:
return True, {'fpga-thermal-zone': 30000}
context = pyudev.Context()
result = {
device.attributes.get('type').decode('ascii'): \
int(device.attributes.get('temp').decode('ascii'))
for device in context.list_devices(subsystem='thermal')
if 'temp' in device.attributes.available_attributes \
and device.attributes.get('temp') is not None
}
return True, result
def bist_fan(self):
"""
BIST for temperature sensors
Description: Reads the RPM values of the fans on the motherboard
Return dictionary:
- : Fan speed in RPM
External Equipment: None
"""
assert 'fan' in self.tests_to_run
if self.args.dry_run:
return True, {'cooling_device0': 10000}
context = pyudev.Context()
result = {
device.sys_name: int(device.attributes.get('cur_state'))
for device in context.list_devices(subsystem='thermal')
if 'cur_state' in device.attributes.available_attributes \
and device.attributes.get('cur_state') is not None
}
return True, result
def main():
" Go, go, go! "
return N310BIST().run()
if __name__ == '__main__':
exit(not main())