diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Revision history for ftdi
 
+## 0.3.0.2 -- 2021-09-25
+
+* Update README with code samples and additional resources
+* Update generic-random, transformers bounds
+
 ## 0.3.0.1 -- 2021-06-19
 
 * Update README
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,15 +1,85 @@
-# ftdi
+# FTDI
 [![Haskell CI](https://github.com/standardsemiconductor/ftdi/workflows/Haskell%20CI/badge.svg?branch=master)](https://github.com/standardsemiconductor/ftdi/actions/workflows/haskell.yml)
 [![Hackage][hackage-badge]][hackage]
 [![Hackage Dependencies][hackage-deps-badge]][hackage-deps]
 
-This library enables you to communicate with FTDI devices. It is implemented as a lightweight wrapper around the [usb](https://hackage.haskell.org/package/usb) library.
+This library enables you to communicate with FTDI USB devices. It is implemented as a lightweight wrapper around the [usb](https://hackage.haskell.org/package/usb) library.
 
 See [bindings-libusb](https://hackage.haskell.org/package/bindings-libusb) for instructions to install [libusb](https://libusb.info).
 
+## Build Source (Ubuntu)
+
+```bash
+$ sudo apt install libusb-1.0-0-dev
+$ git clone https://github.com/standardsemiconductor/ftdi.git
+$ cd ftdi/
+$ cabal build
+$ cabal test
+```
+
+## Sample Usage
+
+Find the first USB device matching the vendor ID and product ID:
+
+```haskell
+import qualified System.USB as USB
+import qualified Data.Vector as V (toList)
+import Data.List (find)
+import System.FTDI
+
+data Failure = FailureNotFound
+             | FailureOther
+
+findUSBDevice 
+  :: USB.VendorId 
+  -> USB.ProductId 
+  -> IO (Either Failure (USB.Device, USB.Ctx))
+findUSBDevice vendorId productId = do
+  ctx <- USB.newCtx
+  devDescs <- getDeviceDescs ctx
+  return $ case fst <$> find (match . snd) devDescs of
+    Nothing -> Left FailureNotFound
+    Just dev -> Right (dev, ctx)
+  where
+    match :: USB.DeviceDesc -> Bool
+    match devDesc =  USB.deviceVendorId  devDesc == vendorId
+                  && USB.deviceProductId devDesc == productId
+
+getDeviceDescs :: USB.Ctx -> IO [(USB.Device, USB.DeviceDesc)]
+getDeviceDescs ctx = do
+  devs <- V.toList <$> USB.getDevices ctx
+  deviceDescs <- mapM USB.getDeviceDesc devs
+  return $ zip devs deviceDescs
+```
+
+Setup an FT2232 FTDI device on interface A using MPSSE (Multi-Protocol Synchronous Serial Engine):
+
+```haskell
+withFTDI 
+  :: USB.VendorId 
+  -> USB.productId
+  -> (InterfaceHandle -> IO (Either Failure a) 
+  -> IO (Either Failure a)
+withFTDI vendorId productId action = findUSBDevice vendorId productId >>= \case
+  Left failure -> return $ Left failure
+  Right (usbDevice, ctx) -> do
+    ftdiDevice <- fromUSBDevice usbDevice ChipType_2232H
+    withDeviceHandle ftdiDevice $ \devHndl -> do
+      resetUSB devHndl
+      withDetachedKernelDriver devHndl Interface_A $
+        withInterfaceHandle devHndl Interface_A $ \ifHndl -> do
+          reset ifHndl
+          purgeReadBuffer ifHndl
+          purgeWriteBuffer ifHndl
+          setLatencyTimer ifHndl 1
+          setBitMode ifHndl 0xFF BitMode_MPSSE
+          action ifHndl
+```
+
 ## References
 * [FTDI Website](https://ftdichip.com/)
 * [Application Note AN_108](https://www.ftdichip.com/Support/Documents/AppNotes/AN_108_Command_Processor_for_MPSSE_and_MCU_Host_Bus_Emulation_Modes.pdf): Command Processor for MPSSE and MCU Host Bus Emulation Modes
+* [USB in a NutShell](https://www.beyondlogic.org/usbnutshell/usb1.shtml): Making sense of the USB standard
 
 [hackage]:            <https://hackage.haskell.org/package/ftdi>
 [hackage-badge]:      <https://img.shields.io/hackage/v/ftdi.svg?color=success>
diff --git a/System/FTDI/Internal.hs b/System/FTDI/Internal.hs
--- a/System/FTDI/Internal.hs
+++ b/System/FTDI/Internal.hs
@@ -358,21 +358,21 @@
              , MonadFix
              )
 
-{-| Run the ChunkedReaderT given an initial state.
+{-| Run the 'ChunkedReaderT' given an initial state.
 
 The initial state represents excess bytes carried over from a previous
-run. When invoking runChunkedReaderT for the first time you can safely pass the
+run. When invoking 'runChunkedReaderT' for the first time you can safely pass the
 'BS.empty' bytestring as the initial state.
 
 A contrived example showing how you can manually thread the excess bytes
-through subsequent invocations of runChunkedReaderT:
+through subsequent invocations of 'runChunkedReaderT':
 
 @
-  example &#x2237; 'InterfaceHandle' &#x2192; IO ()
+  example :: 'InterfaceHandle' -> IO ()
   example ifHnd = do
-    (packets1, rest1) &#x2190; runChunkedReaderT ('readData' ifHnd (return 'False') 400) 'BS.empty'
+    (packets1, rest1) <- runChunkedReaderT ('readData' ifHnd (return 'False') 400) 'BS.empty'
     print $ 'BS.concat' packets1
-    (packets2, rest2) &#x2190; runChunkedReaderT ('readData' ifHnd (return 'False') 200) rest1
+    (packets2, rest2) <- runChunkedReaderT ('readData' ifHnd (return 'False') 200) rest1
     print $ 'BS.concat' packets2
 @
 
@@ -380,12 +380,12 @@
 plumbing:
 
 @
-  example &#x2237; 'InterfaceHandle' &#x2192; IO ()
+  example :: 'InterfaceHandle' -> IO ()
   example ifHnd =
-    let reader = do packets1 &#x2190; 'readData' ifHnd (return 'False') 400
-                    liftIO $ print $ 'BS.concat' packets1
-                    packets2 &#x2190; 'readData' ifHnd (return 'False') 200
-                    liftIO $ print $ 'BS.concat' packets1
+    let reader = do packets1 <- 'readData' ifHnd (return 'False') 400
+                    'liftIO' $ print $ 'BS.concat' packets1
+                    packets2 <- 'readData' ifHnd (return 'False') 200
+                    'liftIO' $ print $ 'BS.concat' packets1
     in runChunkedReaderT reader 'BS.empty'
 @
 
@@ -395,9 +395,9 @@
 
 {-| Reads data from the given FTDI interface by performing bulk reads.
 
-This function produces an action in the @ChunkedReaderT@ monad that
+This function produces an action in the 'ChunkedReaderT' monad that
 will read exactly the requested number of bytes unless it is
-explicitly asked to stop early. Executing the @readData@ action will
+explicitly asked to stop early. Executing the 'readData' action will
 block until either:
 
  * All data are read
@@ -409,16 +409,16 @@
 
 Data are read in packets. The function may choose to request more than
 needed in order to get the highest possible bandwidth. The excess of
-bytes is kept as the state of the @ChunkedReaderT@ monad. A subsequent
-invocation of @readData@ will first return bytes from the stored state
+bytes is kept as the state of the 'ChunkedReaderT' monad. A subsequent
+invocation of 'readData' will first return bytes from the stored state
 before requesting more from the device itself. A consequence of this
 behaviour is that even when you request 100 bytes the function will
 actually request 512 bytes (depending on the packet size) and /block/
 until all 512 bytes are read! There is no workaround since requesting
 less bytes than the packet size is an error.
 
-USB timeouts will not interrupt @readData@. In case of a timeout
-@readData@ will simply resume reading data. A small USB timeout can
+USB timeouts will not interrupt 'readData'. In case of a timeout
+'readData' will simply resume reading data. A small USB timeout can
 degrade performance.
 
 The FTDI latency timer can cause poor performance. If the FTDI chip can't fill
@@ -438,7 +438,7 @@
 
 @
   -- Read 100 data bytes from ifHnd
-  (packets, rest) &#x2190; 'runChunkedReaderT' ('readData' ifHnd (return 'False') 100) 'BS.empty'
+  (packets, rest) <- 'runChunkedReaderT' ('readData' ifHnd (return 'False') 100) 'BS.empty'
 @
 
 -}
diff --git a/System/FTDI/MPSSE.hs b/System/FTDI/MPSSE.hs
--- a/System/FTDI/MPSSE.hs
+++ b/System/FTDI/MPSSE.hs
@@ -54,6 +54,7 @@
 import qualified Data.ByteString.Builder as BSB
 
 import Control.Concurrent.Async
+import Control.Monad (void)
 
 import qualified System.FTDI as FTDI
 import System.FTDI (InterfaceHandle)
@@ -96,11 +97,11 @@
 {-# INLINE opCode #-}
 
 byte :: Word8 -> Command ()
-byte o = () <$ transfer (BSB.word8 o) 0
+byte o = void $ transfer (BSB.word8 o) 0
 {-# INLINE byte #-}
 
 word16 :: Word16 -> Command ()
-word16 o = () <$ transfer (BSB.word16LE o) 0
+word16 o = void $ transfer (BSB.word16LE o) 0
 {-# INLINE word16 #-}
 
 transfer :: BSB.Builder -> Int -> Command BS.ByteString
@@ -110,7 +111,7 @@
 {-# INLINE transfer #-}
 
 writeByteString :: BS.ByteString -> Command ()
-writeByteString bs = () <$ transfer (BSB.byteString bs) 0
+writeByteString bs = void $ transfer (BSB.byteString bs) 0
 {-# INLINE writeByteString #-}
 
 readN :: Int -> Command BS.ByteString
diff --git a/System/FTDI/Properties.hs b/System/FTDI/Properties.hs
--- a/System/FTDI/Properties.hs
+++ b/System/FTDI/Properties.hs
@@ -1,6 +1,5 @@
 {-# LANGUAGE GeneralizedNewtypeDeriving
            , StandaloneDeriving
-           , TemplateHaskell
            , UnicodeSyntax
   #-}
 
diff --git a/ftdi.cabal b/ftdi.cabal
--- a/ftdi.cabal
+++ b/ftdi.cabal
@@ -1,5 +1,5 @@
 name:          ftdi
-version:       0.3.0.1
+version:       0.3.0.2
 cabal-version: >=1.10
 build-type:    Simple
 stability:     experimental
@@ -32,7 +32,7 @@
                , base                 >= 4.5   && < 4.16
                , base-unicode-symbols >= 0.1.1 && < 0.3
                , bytestring           >= 0.10  && < 0.12
-               , transformers         >= 0.5   && < 0.6
+               , transformers         >= 0.5   && < 0.7
                , usb                  >= 1.3   && < 1.4
                , vector               >= 0.12  && < 0.13
   default-language: Haskell2010
@@ -54,7 +54,7 @@
                , base-unicode-symbols
                , bytestring
                , QuickCheck                 >= 2.11  && < 2.15
-               , generic-random             >= 1.3   && < 1.5
+               , generic-random             >= 1.3   && < 1.6
                , random                     >= 1.0.0 && < 1.3
                , tagged                     >= 0.8   && < 0.9
                , test-framework             >= 0.8   && < 0.9
