diff --git a/chipforge/cli.py b/chipforge/cli.py
index e0164b7d4589e7ccd8c373e4106e56c5b458cec9..d314b11e3d3d92c96363fc9c9419cb666a14a294 100644
--- a/chipforge/cli.py
+++ b/chipforge/cli.py
@@ -8,7 +8,7 @@ import pkg_resources
 
 from .autoupdate import autoupdate
 from .fpga import fpga
-from .firmware import flash, projectid, list_devices, free
+from .firmware import flash, projectid, list_devices, free, boot
 from . import colors as c
 
 def get_version():
@@ -47,4 +47,5 @@ cli.add_command(flash)
 cli.add_command(fpga)
 cli.add_command(free)
 cli.add_command(list_devices)
-cli.add_command(projectid)
\ No newline at end of file
+cli.add_command(projectid)
+cli.add_command(boot)
\ No newline at end of file
diff --git a/chipforge/firmware/__init__.py b/chipforge/firmware/__init__.py
index f7ca61669eabfbea5f6a039044763cd0adf8abea..76e1ed2e499313a7daf5c21317911441edb2c7a2 100644
--- a/chipforge/firmware/__init__.py
+++ b/chipforge/firmware/__init__.py
@@ -1,4 +1,5 @@
 from .flash import flash
 from .list import list_devices
 from .free import free
-from .projectid import projectid
\ No newline at end of file
+from .projectid import projectid
+from .boot import boot
\ No newline at end of file
diff --git a/chipforge/firmware/boot.py b/chipforge/firmware/boot.py
new file mode 100644
index 0000000000000000000000000000000000000000..5b69858cef982853974aeafd282bcefd1bf61a20
--- /dev/null
+++ b/chipforge/firmware/boot.py
@@ -0,0 +1,30 @@
+import click
+from .. import colors as c
+from .interface.hk import HKSpiBase, pass_device_with_options
+from .usb_reset import usb_reset
+
+def abort_if_false(ctx, param, value):
+    if not value:
+        ctx.abort()
+
+@click.command()
+@click.option('--yes', is_flag=True, callback=abort_if_false,
+              expose_value=False,
+              prompt='Are you sure you want to boot everyone? That\'s not a very nice thing to do...')
+@pass_device_with_options
+def boot(hk: HKSpiBase):
+  """
+  Boot everyone user off of a usb device
+  """
+  
+  if hasattr(hk, 'sysfs'):
+    if not usb_reset(sysfs=hk.sysfs):
+      click.echo(c.error('BOOT FAILED'))
+      return
+  
+  if hasattr(hk, 'usb_device'):
+    if not usb_reset(busnum=hk.usb_device.bus, devnum=hk.usb_device.address):
+      click.echo(c.error('BOOT FAILED'))
+      return
+
+  click.echo(c.success('SUCCESS'))
\ No newline at end of file
diff --git a/chipforge/firmware/flash.py b/chipforge/firmware/flash.py
index 67b86ac0260d4bed041e22b414d0c3b6b4d969eb..886cd13c370d05544417eacab1f19870776aa8c2 100644
--- a/chipforge/firmware/flash.py
+++ b/chipforge/firmware/flash.py
@@ -20,113 +20,140 @@ def flash(hk: HKSpiBase, hex_path: str):
     HEX_PATH should be either the path to a .hex file, or the path to a folder containing one.
     """
 
-    if os.path.isdir(hex_path):
-        for file in os.listdir(hex_path):
-            if file.endswith('.hex'):
-                hex_path = os.path.join(hex_path, file)
-                break
-        else:
-            click.echo(c.error())
-            click.echo(c.error(f'Could not find a {c.code(".hex")} file in {c.code(hex_path)}'))
-            click.echo(c.error())
-            exit(1)
-
-    hk.identify()
-    hk.cpu_reset_hold()
-
-    time.sleep(0.5)
-    hk.led0.toggle()
-
-    hk.flash_reset()
-    hk.flash_identify()
-
-
-    buf = bytearray()
-    addr = 0
-    nbytes = 0
-    total_bytes = 0
-
-    erased = []
-    def ensure_erased(addr):
-        if (addr & hk.flash_sector_mask) not in erased:
-            erased.append(addr & hk.flash_sector_mask)
-            hk.flash_erase_sector(addr)
-
-    click.echo(c.info('> Programming Flash'))
-    with open(hex_path, mode='r') as f:
-        x = f.readline()
-        while x != '':
-            if x[0] == '@':
-                addr = int(x[1:],16)
-                click.echo(f'  Jumping to address 0x{addr:x}')
+    with hk:
+        if os.path.isdir(hex_path):
+            for file in os.listdir(hex_path):
+                if file.endswith('.hex'):
+                    hex_path = os.path.join(hex_path, file)
+                    break
             else:
-                # click.echo(x)
-                values = bytearray.fromhex(x[0:len(x)-1])
-                buf[nbytes:nbytes] = values
-                nbytes += len(values)
-                # click.echo(binascii.hexlify(values))
+                click.echo(c.error())
+                click.echo(c.error(f'Could not find a {c.code(".hex")} file in {c.code(hex_path)}'))
+                click.echo(c.error())
+                exit(1)
 
-            x = f.readline()
+        hk.identify()
+        hk.cpu_reset_hold()
+
+        time.sleep(0.5)
+        hk.led0.toggle()
+
+        hk.flash_reset()
+        hk.flash_identify()
 
-            if nbytes >= 256 or (x != '' and x[0] == '@' and nbytes > 0):
+
+        buf = bytearray()
+        addr = 0
+        nbytes = 0
+        total_bytes = 0
+
+        erased = []
+        def ensure_erased(addr):
+            if (addr & hk.flash_sector_mask) not in erased:
+                erased.append(addr & hk.flash_sector_mask)
+                hk.flash_erase_sector(addr)
+
+        click.echo(c.info('> Programming Flash'))
+        with open(hex_path, mode='r') as f:
+            x = f.readline()
+            while x != '':
+                if x[0] == '@':
+                    addr = int(x[1:],16)
+                    click.echo(f'  Jumping to address 0x{addr:x}')
+                else:
+                    # click.echo(x)
+                    values = bytearray.fromhex(x[0:len(x)-1])
+                    buf[nbytes:nbytes] = values
+                    nbytes += len(values)
+                    # click.echo(binascii.hexlify(values))
+
+                x = f.readline()
+
+                if nbytes >= 256 or (x != '' and x[0] == '@' and nbytes > 0):
+                    total_bytes += nbytes
+                    # click.echo('\n----------------------\n')
+                    # click.echo(binascii.hexlify(buf))
+                    # click.echo('\ntotal_bytes = {}'.format(total_bytes))
+
+                    ensure_erased(addr)
+                    hk.flash_write_page(addr, buf)
+
+                    if nbytes > 256:
+                        buf = buf[255:]
+                        addr += 256
+                        nbytes -= 256
+                        click.echo('*** over 256 hit')
+                    else:
+                        buf = bytearray()
+                        addr += 256
+                        nbytes =0
+
+            if nbytes > 0:
                 total_bytes += nbytes
                 # click.echo('\n----------------------\n')
                 # click.echo(binascii.hexlify(buf))
-                # click.echo('\ntotal_bytes = {}'.format(total_bytes))
+                # click.echo('\nnbytes = {}'.format(nbytes))
 
                 ensure_erased(addr)
                 hk.flash_write_page(addr, buf)
 
-                if nbytes > 256:
-                    buf = buf[255:]
-                    addr += 256
-                    nbytes -= 256
-                    click.echo('*** over 256 hit')
-                else:
-                    buf = bytearray()
-                    addr += 256
-                    nbytes =0
-
-        if nbytes > 0:
-            total_bytes += nbytes
-            # click.echo('\n----------------------\n')
-            # click.echo(binascii.hexlify(buf))
-            # click.echo('\nnbytes = {}'.format(nbytes))
+        click.echo(c.success(f'> Programmed {c.code(total_bytes)} total bytes'))
 
-            ensure_erased(addr)
-            hk.flash_write_page(addr, buf)
+        click.echo(c.info('> Verifying Flash'))
 
-    click.echo(c.success(f'> Programmed {c.code(total_bytes)} total bytes'))
+        buf = bytearray()
+        addr = 0
+        nbytes = 0
+        total_bytes = 0
 
-    click.echo(c.info('> Verifying Flash'))
+        while (hk.is_busy()):
+            time.sleep(0.5)
 
-    buf = bytearray()
-    addr = 0
-    nbytes = 0
-    total_bytes = 0
+        # slave.write([CARAVEL_REG_WRITE, 0x0b, 0x01])
+        # slave.write([CARAVEL_REG_WRITE, 0x0b, 0x00])
 
-    while (hk.is_busy()):
-        time.sleep(0.5)
-
-    # slave.write([CARAVEL_REG_WRITE, 0x0b, 0x01])
-    # slave.write([CARAVEL_REG_WRITE, 0x0b, 0x00])
-
-    with open(hex_path, mode='r') as f:
-        x = f.readline()
-        while x != '':
-            if x[0] == '@':
-                addr = int(x[1:],16)
-                click.echo(f'  Jumping to address 0x{addr:x}')
-            else:
-                values = bytearray.fromhex(x[0:len(x)-1])
-                buf[nbytes:nbytes] = values
-                nbytes += len(values)
+        with open(hex_path, mode='r') as f:
             x = f.readline()
-
-            if nbytes >= 256 or (x != '' and x[0] == '@' and nbytes > 0):
+            while x != '':
+                if x[0] == '@':
+                    addr = int(x[1:],16)
+                    click.echo(f'  Jumping to address 0x{addr:x}')
+                else:
+                    values = bytearray.fromhex(x[0:len(x)-1])
+                    buf[nbytes:nbytes] = values
+                    nbytes += len(values)
+                x = f.readline()
+
+                if nbytes >= 256 or (x != '' and x[0] == '@' and nbytes > 0):
+                    total_bytes += nbytes
+
+                    read_cmd = bytearray((CARAVEL_PASSTHRU, CMD_READ_LO_SPEED,(addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff))
+                    buf2 = hk.slave.exchange(read_cmd, nbytes)
+                    if buf == buf2:
+                        click.echo(f'  Page 0x{addr:x} is valid')
+                    else:
+                        click.echo(c.error())
+                        click.echo(c.error(f'Page 0x{addr:x} compare failed'))
+                        click.echo(c.error(binascii.hexlify(buf)))
+                        click.echo(c.error('<----->'))
+                        click.echo(c.error(binascii.hexlify(buf2)))
+                        click.echo(c.error())
+                        exit(1)
+
+                    if nbytes > 256:
+                        buf = buf[255:]
+                        addr += 256
+                        nbytes -= 256
+                        click.echo('*** over 256 hit')
+                    else:
+                        buf = bytearray()
+                        addr += 256
+                        nbytes =0
+
+            if nbytes > 0:
                 total_bytes += nbytes
 
-                read_cmd = bytearray((CARAVEL_PASSTHRU, CMD_READ_LO_SPEED,(addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff))
+                read_cmd = bytearray((CARAVEL_PASSTHRU, CMD_READ_LO_SPEED, (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff))
                 buf2 = hk.slave.exchange(read_cmd, nbytes)
                 if buf == buf2:
                     click.echo(f'  Page 0x{addr:x} is valid')
@@ -139,36 +166,12 @@ def flash(hk: HKSpiBase, hex_path: str):
                     click.echo(c.error())
                     exit(1)
 
-                if nbytes > 256:
-                    buf = buf[255:]
-                    addr += 256
-                    nbytes -= 256
-                    click.echo('*** over 256 hit')
-                else:
-                    buf = bytearray()
-                    addr += 256
-                    nbytes =0
-
-        if nbytes > 0:
-            total_bytes += nbytes
-
-            read_cmd = bytearray((CARAVEL_PASSTHRU, CMD_READ_LO_SPEED, (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff))
-            buf2 = hk.slave.exchange(read_cmd, nbytes)
-            if buf == buf2:
-                click.echo(f'  Page 0x{addr:x} is valid')
-            else:
-                click.echo(c.error())
-                click.echo(c.error(f'Page 0x{addr:x} compare failed'))
-                click.echo(c.error(binascii.hexlify(buf)))
-                click.echo(c.error('<----->'))
-                click.echo(c.error(binascii.hexlify(buf2)))
-                click.echo(c.error())
-                exit(1)
+        click.echo(c.success(f'> Verified {c.code(total_bytes)} total bytes'))
 
-    click.echo(c.success(f'> Verified {c.code(total_bytes)} total bytes'))
+        click.echo(f'\nUART port is at {c.code(hk.uart_port)}')
 
-    hk.cpu_reset_release()
+        hk.cpu_reset_release()
 
-    hk.led0.toggle()
-    time.sleep(0.3)
-    hk.led0.toggle()
\ No newline at end of file
+        hk.led0.toggle()
+        time.sleep(0.3)
+        hk.led0.toggle()
\ No newline at end of file
diff --git a/chipforge/firmware/interface/ftdi.py b/chipforge/firmware/interface/ftdi.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f5ac08f27bd758135e195a18423ad4b05a62f76
--- /dev/null
+++ b/chipforge/firmware/interface/ftdi.py
@@ -0,0 +1,11 @@
+from pyftdi.ftdi import Ftdi
+from pyftdi.usbtools import UsbTools, UsbDeviceDescriptor
+from serial.tools.list_ports import comports
+from ..usb_reset import get_bus_dev_from_sysfs_path
+
+def get_ftdi_url(dev: UsbDeviceDescriptor):
+  return UsbTools.build_dev_strings('ftdi', Ftdi.VENDOR_IDS, Ftdi.PRODUCT_IDS, [(dev, 1)])[0][0]
+
+def get_uart_port(dev: UsbDeviceDescriptor):
+  ports = [port for port in comports() if port.vid == 0x0403 and port.pid == dev.pid and port.serial_number == dev.sn and get_bus_dev_from_sysfs_path(port.device_path) == (dev.bus, dev.address)]
+  return ports[-1].device
\ No newline at end of file
diff --git a/chipforge/firmware/interface/hk.py b/chipforge/firmware/interface/hk.py
index 2f89751585836aab85e044fd223aeaee5d57974c..a7180935ab4dad644bb3c46ea58252aec09cfb46 100644
--- a/chipforge/firmware/interface/hk.py
+++ b/chipforge/firmware/interface/hk.py
@@ -43,8 +43,7 @@ def pass_device_with_options(fn):
     else:
       drivers = ALL_DRIVERS
 
-    with HKSpi(drivers=drivers, serial_number=serial_number, device_id=device_id, override=override) as hk:
-      return fn(hk, *args, **kwargs)
+    fn(HKSpi(drivers=drivers, serial_number=serial_number, device_id=device_id, override=override), *args, **kwargs)
   wrapped_fn.__name__ = fn.__name__
   wrapped_fn.__doc__ = fn.__doc__
     
diff --git a/chipforge/firmware/interface/hk_base.py b/chipforge/firmware/interface/hk_base.py
index 6d7a2b243f82c471fe5855c8d15b2add80d83067..557e4039dd3188915c35111096ebf9d6c1c47b0e 100644
--- a/chipforge/firmware/interface/hk_base.py
+++ b/chipforge/firmware/interface/hk_base.py
@@ -23,7 +23,6 @@ class HKSpiBase:
         serial_number = '' if self.serial_number == None else f'({c.default(self.serial_number)})'
         return f'{c.code(self.device_id)} {serial_number} \t- {c.warning(self.__class__.__name__)} ({self.description})'
 
-
     def identify(self):
         click.echo(c.info('> Caravel Hardware Info:'))
 
diff --git a/chipforge/firmware/interface/hk_ft232h.py b/chipforge/firmware/interface/hk_ft232h.py
index 974cd6bd330a8d7e78b6f2c0a138cdf1a5431252..821477f01bff23044486f2bb1c63bf10a5e46e87 100644
--- a/chipforge/firmware/interface/hk_ft232h.py
+++ b/chipforge/firmware/interface/hk_ft232h.py
@@ -2,16 +2,19 @@ from .gpio import Gpio
 from .defs import *
 from pyftdi.ftdi import Ftdi
 from pyftdi.spi import SpiController
-from pyftdi.usbtools import UsbTools
+from pyftdi.usbtools import UsbDeviceDescriptor
 from .hk_base import HKSpiBase
+from .ftdi import get_ftdi_url, get_uart_port
 
 class HKSpiFT232H(HKSpiBase):
     description = 'eFabless Caravel Breakout'
 
-    def __init__(self, url: str, serial_number: str):
-        self.serial_number = serial_number
-        self.device_id = url
-    
+    def __init__(self, dev: UsbDeviceDescriptor):
+        self.usb_device = dev
+        self.serial_number = dev.sn
+        self.device_id = get_ftdi_url(dev)
+        self.uart_port = get_uart_port(dev)
+
     def __enter__(self):
         self.spi = SpiController(cs_count=1)
         self.spi.configure(self.device_id)
@@ -39,5 +42,5 @@ class HKSpiFT232H(HKSpiBase):
         # Find FT232H FTDI chips
         ftdidevs = [d for d in Ftdi.list_devices() if d[0].description == 'Single RS232-HS']
         # build_dev_strings returns [(url, descriptor)], we only want the URL
-        return [HKSpiFT232H(UsbTools.build_dev_strings('ftdi', Ftdi.VENDOR_IDS, Ftdi.PRODUCT_IDS, (d,))[0][0], d[0].sn) for d in ftdidevs]
+        return [HKSpiFT232H(d[0]) for d in ftdidevs]
 
diff --git a/chipforge/firmware/interface/hk_pico.py b/chipforge/firmware/interface/hk_pico.py
index 499e40439afc017b4bb33f351d214d6edad4495d..b417bcad535b0135c9168673bdeabda311c4f056 100644
--- a/chipforge/firmware/interface/hk_pico.py
+++ b/chipforge/firmware/interface/hk_pico.py
@@ -1,3 +1,4 @@
+from ..usb_reset import usb_reset
 from .hk_base import HKSpiBase, DummyLED
 from serial import Serial
 from serial.tools.list_ports import comports
@@ -11,12 +12,10 @@ class PicoSPI:
 
   def open(self):
     if self.port == None:
-      print([repr(f) for f in comports()])
-      self.port = next((f.device for f in comports() if f.description == 'STM32 USB-SPI Adapter'), None)
-      if self.port == None:
-        print('Could not find Pico SPI adapter')
-        exit(1)
+      print('Could not find Pico SPI adapter')
+      exit(1)
 
+    usb_reset(sysfs=self.sysfs)
     self.serial = Serial(self.port)
 
   def close(self):
@@ -63,9 +62,11 @@ class PicoSPI:
 class HKSpiPico(HKSpiBase):
   description = 'ISU Chip Fab Caravel PMOD Breakout'
 
-  def __init__(self, comport):
-    self.serial_number = comport.serial_number
-    self.device_id = comport.device
+  def __init__(self, sysfs):
+    self.sysfs = sysfs
+    self.serial_number = sysfs.serial_number
+    self.device_id = sysfs.device
+    self.uart_port = next((f.device for f in comports() if f.location == sysfs.location[:-2] + '.0'), None)
     self.slave = PicoSPI(self.device_id)
     self.led0 = DummyLED()
     self.led1 = DummyLED()
diff --git a/chipforge/firmware/interface/hk_stm32.py b/chipforge/firmware/interface/hk_stm32.py
index c038dc85bc27ad6a0a14537bb531f2a059de0988..c50789207c268f493a47cf75b6e3605bc747b0c7 100644
--- a/chipforge/firmware/interface/hk_stm32.py
+++ b/chipforge/firmware/interface/hk_stm32.py
@@ -11,10 +11,8 @@ class STM32SPI:
 
   def open(self):
     if self.port == None:
-      self.port = next((f.device for f in comports() if f.description == 'STM32 USB-SPI Adapter'), None)
-      if self.port == None:
-        print('Could not find USB SPI adapter')
-        exit(1)
+      print('Could not find Pico SPI adapter')
+      exit(1)
 
     self.serial = Serial(self.port, rtscts=True)
 
@@ -67,9 +65,11 @@ class STM32SPI:
 class HKSpiSTM32(HKSpiBase):
   description = 'Caravel FPGA'
 
-  def __init__(self, comport):
-    self.serial_number = comport.serial_number
-    self.device_id = comport.device
+  def __init__(self, sysfs):
+    self.sysfs = sysfs
+    self.serial_number = sysfs.serial_number
+    self.device_id = sysfs.device
+    self.uart_port = None
     self.slave = STM32SPI(self.device_id)
     self.led0 = DummyLED()
     self.led1 = DummyLED()
diff --git a/chipforge/firmware/interface/hk_xilinx.py b/chipforge/firmware/interface/hk_xilinx.py
index 39631a5ff5a7c3315f24f69fe66c3785ab10a813..8ba8a005edab2fdfd65b9c3eaec7c32c5e777f0a 100644
--- a/chipforge/firmware/interface/hk_xilinx.py
+++ b/chipforge/firmware/interface/hk_xilinx.py
@@ -2,7 +2,8 @@ from .hk_base import HKSpiBase, DummyLED
 from pyftdi.jtag import JtagEngine
 from pyftdi.bits import BitSequence
 from pyftdi.ftdi import Ftdi
-from pyftdi.usbtools import UsbTools
+from pyftdi.usbtools import UsbDeviceDescriptor
+from .ftdi import get_ftdi_url, get_uart_port
 
 def hexdump(data):
   return " ".join(f'{d:02x}' for d in data)
@@ -19,11 +20,8 @@ class XilinxJtagSPI:
 
   def open(self):
     if self.url == None:
-      try:
-        self.url = HKSpiXilinx.list_devices()[0].device_id
-      except:
-        print('Could not find a Xilinx JTAG adapter')
-        exit(1)
+      print('Could not find a Xilinx JTAG adapter')
+      exit(1)
     
     self.jtag = JtagEngine(frequency=3E4)
     self.jtag.configure(self.url)
@@ -95,9 +93,11 @@ class XilinxJtagSPI:
 class HKSpiXilinx(HKSpiBase):
   description = 'Xilinx FPGA'
 
-  def __init__(self, url: str, serial_number: str):
-    self.serial_number = serial_number
-    self.device_id = url
+  def __init__(self, dev: UsbDeviceDescriptor):
+    self.usb_device = dev
+    self.serial_number = dev.sn    
+    self.device_id = get_ftdi_url(dev)
+    self.uart_port = get_uart_port(dev)
     self.slave = XilinxJtagSPI(self.device_id)
     self.led0 = DummyLED()
     self.led1 = DummyLED()
@@ -116,5 +116,5 @@ class HKSpiXilinx(HKSpiBase):
       # Find Xilinx FPGA boards      
       ftdidevs = [d for d in Ftdi.list_devices() if d[0].description == 'Digilent USB Device']
       # build_dev_strings returns [(url, descriptor)], we only want the URL
-      return [HKSpiXilinx(UsbTools.build_dev_strings('ftdi', Ftdi.VENDOR_IDS, Ftdi.PRODUCT_IDS, (d,))[0][0], d[0].sn) for d in ftdidevs]
+      return [HKSpiXilinx(d[0]) for d in ftdidevs]
 
diff --git a/chipforge/firmware/list.py b/chipforge/firmware/list.py
index 0b7f24ac829ac95c9a086aa9f7349e3f679ae89d..5062608ab6cf7b73b595c60f982ee04da7631fb1 100644
--- a/chipforge/firmware/list.py
+++ b/chipforge/firmware/list.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python3
 import click
+from .. import colors as c
 
 from .interface.defs import *
 from .interface.hk import ALL_DRIVERS, Claims, HKSpiBase
@@ -12,5 +12,10 @@ def list_devices():
     
     with Claims() as claims:
         devices = [device for driver in ALL_DRIVERS for device in driver.list_devices()]
-        for i, device in enumerate(devices):
-            click.echo(f' {i}) {device} {claims.get_owner_string(device)}')
\ No newline at end of file
+        for i, d in enumerate(devices):
+            serial_number = '' if d.serial_number == None else f' ({c.default(d.serial_number)})'
+            
+            click.echo(f' {i}) {c.code(d.device_id)}{serial_number} {claims.get_owner_string(d)}'
+                        + f'\n     {c.warning(d.__class__.__name__)} ({d.description})' 
+                        + f'\n     UART port: {c.code(d.uart_port)}'
+                        + '\n')
\ No newline at end of file
diff --git a/chipforge/firmware/projectid.py b/chipforge/firmware/projectid.py
index e9b42aa7785366086787e456107b872837ea85cd..8e6b662c76ff529c8e25f416831d3d625c4b20ed 100644
--- a/chipforge/firmware/projectid.py
+++ b/chipforge/firmware/projectid.py
@@ -9,4 +9,5 @@ def projectid(hk: HKSpiBase):
     """
     Read the project ID of a physical board
     """
-    hk.identify()
\ No newline at end of file
+    with hk:
+        hk.identify()
\ No newline at end of file
diff --git a/chipforge/firmware/usb_reset.py b/chipforge/firmware/usb_reset.py
new file mode 100644
index 0000000000000000000000000000000000000000..a922232167fea9e2df052de804f87262314df4ed
--- /dev/null
+++ b/chipforge/firmware/usb_reset.py
@@ -0,0 +1,33 @@
+import os
+import fcntl
+
+def get_bus_dev_from_sysfs_path(sysfs_path: str):
+  path = os.path.realpath(sysfs_path)
+  busnum = None
+  devnum = None
+  while path:
+    if os.path.exists(os.path.join(path, 'busnum')):
+      with open(os.path.join(path, 'busnum')) as busnum_file:
+        busnum = int(busnum_file.readline())
+      with open(os.path.join(path, 'devnum')) as devnum_file:
+        devnum = int(devnum_file.readline())
+      break
+    path = os.path.dirname(path)
+  return (busnum, devnum)
+
+def usb_reset(busnum=None, devnum=None, sysfs=None, sysfs_path=None, tty=None):
+  if tty:
+    sysfs_path = f'/sys/class/tty/{tty}/device'
+
+  if sysfs:
+    sysfs_path = sysfs.device_path
+
+  if sysfs_path:
+    busnum, devnum = get_bus_dev_from_sysfs(sysfs_path)
+
+  if busnum == None or devnum == None:
+    return False
+
+  with open(f'/dev/bus/usb/{("000" + str(busnum))[-3:]}/{("000" + str(devnum))[-3:]}', 'w', os.O_WRONLY) as port:
+    fcntl.ioctl(port, 21780)
+    return True