Skip to content

Instantly share code, notes, and snippets.

@benfry
Last active October 3, 2024 01:24
Show Gist options
  • Save benfry/304938b9a4e27ded65c0b36eb70b8272 to your computer and use it in GitHub Desktop.
Save benfry/304938b9a4e27ded65c0b36eb70b8272 to your computer and use it in GitHub Desktop.
Detect USB speeds and report via browser
#!/usr/bin/env python3
import json
import sys
''' Quick hack to detect the speed of plugged-in USB devices on macOS,
then write a simple HTML page with the info and open it in a browser.
Calls out to the command line system_profiler tool to retrieve the information.
Was frustrated by needing to visit
 → About This Mac → More Info… → (scroll down to) System Report → Hardware → USB
whenever trying to figure out whether a device or cable was working as expected.
Ben Fry (ben at fathom dot info) 2024-09-29
'''
# TODO can't find a proper Apple reference for all of these
# missing entries for 20 and 40
# unsure how dual vs single channel is denoted (i.e. 1x20 vs 2x10, or 1x40 vs 2x20)
# https://en.wikipedia.org/wiki/USB_communications#Signaling_rate_(transmission_rate)
SPEEDS = {
'low_speed': 'Up to 1.5 Mb/s',
'full_speed': 'Up to 12 Mb/s',
'high_speed': 'Up to 480 Mb/s',
'super_speed': 'Up to 5 Gb/sec',
'super_speed_plus': 'Up to 10 Gb/s',
}
def process_exec(args):
''' Simple wrapper on subprocess to just get stdout and stderr.
'''
from subprocess import Popen, PIPE
p = Popen(args, stdout=PIPE, stderr=PIPE)
(out_data, err_data) = p.communicate()
result = p.wait()
return (result, out_data.decode('utf-8'), err_data.decode('utf-8'))
def decode(blob, found):
''' Walk the dictionary recursively to get device entries.
'''
for entry in blob:
if '_items' in entry:
decode(entry['_items'], found)
elif 'host_controller' in entry:
# not personally interested in the hubs
pass
else:
found.append(entry)
def make_html(found):
''' Export an HTML page with 90s-era <table> syntax.
'''
outgoing = '''
<html lang="en-US"><head><meta charset="utf-8"/>
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet">
<style>html * { font: 14px Roboto; color: #444 }</style>
<style>a { color: #822 } .less { color: #777 } </style>
</head>
<body>
<table style="margin-left: auto; margin-right: auto;" cellpadding="4">
'''
for item in found:
if 'device_speed' in item:
outgoing += f"""<tr>
<td class="less"> {item.get('manufacturer', '')} </td>
<td style="font-weight: 700"> {item['_name']} </td>
<td> {SPEEDS.get(item['device_speed'], '')} </td>
<td> <tt class="less">{item['device_speed']}</tt> </td>
"""
if 'bus_power' in item:
outgoing += '<td>'
bus_power = int(item['bus_power'])
if 'bus_power_used' in item:
bus_power_used = int(item['bus_power_used'])
else:
# bus_power_used = bus_power
print('bus power used not present for ' + item['_name'])
exit(1)
width_max = 180 # 180px is probably a good width
divisor = 900 // width_max # 900 is gonna be the max mA
bus_power_px = bus_power // divisor
bus_power_used_px = bus_power_used // divisor
remainder_px = bus_power_px - bus_power_used_px
outgoing += f'<div style="background-color: #822; height: 11px; display: inline-block; width: {bus_power_used_px}px"> </div>'
outgoing += f'<div style="background-color: #b77; height: 11px; display: inline-block; width: {remainder_px}px"> </div>'
outgoing += f'<span class="less"> {item["bus_power_used"]} mA </span'
outgoing += '</td>'
outgoing += '</tr>'
else:
print(f"Skipping {item['_name']} because no speed listed.")
outgoing += '''
</table>
</body>
</html>
'''
return outgoing
def write_temp(found):
''' Write the HTML to the temp folder so it's just cleaned up later.
'''
import os
import tempfile
fd, path = tempfile.mkstemp(suffix='.html')
try:
with os.fdopen(fd, 'w') as tmp:
# do stuff with temp file
tmp.write(make_html(found))
process_exec(['open', path])
except Exception as e:
print(e)
finally:
# os.remove(path)
pass
if __name__ == "__main__":
(result, out, err) = process_exec([
'system_profiler',
'-json',
'-detailLevel', 'full',
'SPUSBDataType'
])
if len(err.strip()) > 0:
print(err, file=sys.stderr)
if result == 0:
found = [ ]
decode(json.loads(out)['SPUSBDataType'], found)
write_temp(found)
exit(result)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment