Last active
October 3, 2024 01:24
-
-
Save benfry/304938b9a4e27ded65c0b36eb70b8272 to your computer and use it in GitHub Desktop.
Detect USB speeds and report via browser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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