diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for sport
 
+## 1.0.0.0 -- 4-30-2026
+
+* Fix `SportAlreadyOpen` bug
+* Update exception types
+* Add `isOpenSport` to STM API
+
 ## 0.2.0.0 -- 4-21-2026
 
 * Expose current configuration in STM
diff --git a/exe/Main.hs b/exe/Main.hs
new file mode 100644
--- /dev/null
+++ b/exe/Main.hs
@@ -0,0 +1,6 @@
+module Main where
+
+import Sport.Main
+
+main :: IO ()
+main = sportMain
diff --git a/lib/Sport.hs b/lib/Sport.hs
--- a/lib/Sport.hs
+++ b/lib/Sport.hs
@@ -17,6 +17,7 @@
   , runSport
   , -- *** Open & Close
     openSport
+  , isOpenSport
   , getSportCfg
   , defSportCfg
   , SportCfg(..)
diff --git a/lib/Sport/Sport.hs b/lib/Sport/Sport.hs
--- a/lib/Sport/Sport.hs
+++ b/lib/Sport/Sport.hs
@@ -5,6 +5,7 @@
   , withSport
   , runSport
   , openSport
+  , isOpenSport
   , getSportCfg
   , defSportCfg
   , SportCfg(..)
@@ -52,6 +53,14 @@
   | Open SportCfg Handle
   deriving Eq
 
+isOpen :: State -> Bool
+isOpen Open{} = True
+isOpen _      = False
+
+-- | Check if serial port is open
+isOpenSport :: Sport -> STM Bool
+isOpenSport = fmap isOpen . readTVar . state
+
 data RdCmd
   = Rd Int (TMVar (Either SomeException ByteString))
   | RdSome Int (TMVar (Either SomeException ByteString))
@@ -71,7 +80,7 @@
   , rtimeout   :: Maybe Int
   , excl       :: Bool -- ^ exclusive
   }
-  deriving Eq
+  deriving (Eq, Show)
 
 -- | Binary with no buffering
 defSportCfg :: SportCfg
@@ -90,13 +99,14 @@
 -- | Open the serial port. Throw 'SportAlreadyOpen' if the
 -- serial port was already opened.
 openSport :: Sport -> SportCfg -> IO ()
-openSport (Sport s _ _) cfg = do
+openSport (Sport s _ _) cfg' = do
   res <- newEmptyTMVarIO
   atomically $ do
     st <- readTVar s
     case st of
-      Closed -> writeTVar s $ Opening cfg res
-      _      -> throwSTM SportAlreadyOpen
+      Closed        -> writeTVar s $ Opening cfg' res
+      Opening cfg _ -> throwSTM $ SportAlreadyOpen $ path cfg
+      Open    cfg _ -> throwSTM $ SportAlreadyOpen $ path cfg
   atomically $ do
     result <- takeTMVar res
     case result of
@@ -198,9 +208,12 @@
 
 runState :: Sport -> State -> IO ()
 runState s st = case st of
-  Closed          -> waitNewState s st
-  Opening cfg res -> handleException res $ opening s cfg res
-  Open _ serial   -> runConcurrently $ asum $ map Concurrently
+  Closed -> waitNewState s st
+  Opening cfg res ->
+    handleException res $
+      opening s cfg res
+        `onException` atomically (writeTVar (state s) Closed)
+  Open _ serial -> runConcurrently $ asum $ map Concurrently
     [ waitNewState s st
     , readHandler s serial
     , writeHandler s serial
@@ -259,7 +272,12 @@
     -- | User required an open sport but it was closed.
   = SportClosed
     -- | User attempted to open a serial port that was already open.
-  | SportAlreadyOpen
-  deriving (Eq, Read, Show)
+  | SportAlreadyOpen String
+  deriving Eq
+
+instance Show SportException where
+  show e = "sport exception: " <> case e of
+    SportClosed -> "serial port closed"
+    SportAlreadyOpen p -> "serial port already open: " <> p
 
 instance Exception SportException
diff --git a/sport.cabal b/sport.cabal
--- a/sport.cabal
+++ b/sport.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               sport
-version:            0.2.0.0
+version:            1.0.0.0
 synopsis:           UNIX serial port
 description:        Concurrent serial IO
 license:            MIT
@@ -18,7 +18,7 @@
   location: https://github.com/standardsemiconductor/sport
 
 library
-  ghc-options:      -Wall -O2
+  ghc-options:      -Wall -Wunused-packages -O2
   default-language: Haskell2010
   hs-source-dirs:   lib
   exposed-modules:  Sport,
@@ -31,19 +31,22 @@
                     stm,
                     unix
 
---executable sport
---  ghc-options:      -Wall -O2 -threaded
---  default-language: Haskell2010
---  hs-source-dirs:   exe
---  main-is:          Main.hs
---  build-depends:    base,
---                    sport
+executable sport
+  ghc-options:      -Wall -O2 -threaded
+  default-language: Haskell2010
+  hs-source-dirs:   exe
+  main-is:          Main.hs
+  build-depends:    base,
+                    sport
 
---test-suite test
---  ghc-options:      -Wall
---  default-language: Haskell2010
---  hs-source-dirs:   test
---  type:             exitcode-stdio-1.0
---  main-is:          Main.hs
---  build-depends:    base,
---                    sport
+test-suite test
+  ghc-options:      -Wall -Wunused-packages -O2 -threaded
+  default-language: Haskell2010
+  hs-source-dirs:   test
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  build-depends:    async,
+                    base,
+                    bytestring,
+                    sport,
+                    stm
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,26 @@
+module Main (main) where
+
+import Control.Concurrent.Async
+import Control.Concurrent.STM
+import qualified Data.ByteString.Lazy as BS
+import Sport
+import System.IO
+
+main :: IO ()
+main = do
+  hSetBuffering stdout NoBuffering
+  putStrLn "SPORT TEST"
+  s <- newSportIO
+  race_ (runSport s) $ do
+    putStr "OPENING..."
+    openSport s defSportCfg{speed=B19200}
+    putStrLn "DONE"
+    let bs = BS.replicate 256 0xDC
+    putStr "XFERING..."
+    bs' <- fst <$> concurrently (readSport s 10) (writeSport s bs)
+    putStrLn "DONE"
+    putStr "COMPARING..."
+    putStrLn $ if BS.take 10 bs == bs' then "PASS" else "FAIL"
+    putStr "CLOSING..."
+    closeSport s
+    putStrLn "DONE"
