packages feed

sport-1.0.0.0: lib/Sport/Sport.hs

module Sport.Sport
  ( Sport
  , newSportIO
  , newSport
  , withSport
  , runSport
  , openSport
  , isOpenSport
  , getSportCfg
  , defSportCfg
  , SportCfg(..)
  , closeSport
  , readSport
  , readSomeSport
  , writeSport
  , SportException(..)
  ) where

import Control.Applicative
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import qualified Data.ByteString as Strict
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BS
import Data.Functor
import qualified Sport.Serial as S
import System.IO
import System.Posix

data Sport = Sport
  { state :: TVar State
  , rcmd  :: TMVar RdCmd
  , wcmd  :: TMVar WrCmd
  }

-- | Acquire IO serial port
newSportIO :: IO Sport
newSportIO = atomically newSport

-- | Acquire STM serial port
newSport :: STM Sport
newSport =
  Sport
    <$> newTVar Closed
    <*> newEmptyTMVar
    <*> newEmptyTMVar

data State
  = Closed
  | Opening SportCfg (TMVar (Either SomeException ()))
  | 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))

data WrCmd
  = Wr ByteString (TMVar (Either SomeException ()))

-- | Handle and serial configuration
data SportCfg = SportCfg
  { binaryMode :: Bool -- ^ Binary mode True, text mode False.
  , bufferMode :: BufferMode
  , path       :: FilePath
  , speed      :: BaudRate
  , byteSize   :: Int -- ^ number of bits per byte
  , parity     :: Maybe S.Parity
  , stopBits   :: S.StopBits
  , rtimeout   :: Maybe Int
  , excl       :: Bool -- ^ exclusive
  }
  deriving (Eq, Show)

-- | Binary with no buffering
defSportCfg :: SportCfg
defSportCfg =
  SportCfg
    True
    NoBuffering
    (S.path S.defSerialCfg)
    (S.speed S.defSerialCfg)
    (S.byteSize S.defSerialCfg)
    (S.parity S.defSerialCfg)
    (S.stopBits S.defSerialCfg)
    (S.rtimeout S.defSerialCfg)
    (S.excl S.defSerialCfg)

-- | Open the serial port. Throw 'SportAlreadyOpen' if the
-- serial port was already opened.
openSport :: Sport -> SportCfg -> IO ()
openSport (Sport s _ _) cfg' = do
  res <- newEmptyTMVarIO
  atomically $ do
    st <- readTVar s
    case st of
      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
      Left err -> throwSTM err
      Right () -> return ()

-- | Get current configuration.
getSportCfg :: Sport -> STM (Maybe SportCfg)
getSportCfg (Sport s _ _) = do
  st <- readTVar s
  return $ case st of
    Closed        -> Nothing
    Opening cfg _ -> Just cfg
    Open    cfg _ -> Just cfg

-- | Close the serial port.
closeSport :: Sport -> IO ()
closeSport (Sport s r w) = join $ atomically $ do
  st <- readTVar s
  writeTVar s Closed
  (rM, wM) <- (,) <$> tryTakeTMVar r <*> tryTakeTMVar w
  forM_ rM $ \cmd -> case cmd of
    Rd     _ res -> closeResponse res
    RdSome _ res -> closeResponse res
  forM_ wM $ \(Wr _ res) -> closeResponse res
  case st of
    Closed        -> return $ return ()
    Opening _ res -> closeResponse res $> return ()
    Open _ serial -> return $ hClose serial

closeResponse :: TMVar (Either SomeException a) -> STM ()
closeResponse res = void $ tryPutTMVar res $ Left $ toException SportClosed

-- | Read n bytes from the serial port. If the serial port is
-- closed then throw 'SportClosed'. Block if the serial port is
-- busy handling a concurrent request or until all n bytes are available.
readSport :: Sport -> Int -> IO ByteString
readSport (Sport s r _) n = do
  res <- newEmptyTMVarIO
  atomically $ do
    st <- readTVar s
    case st of
      Closed -> throwSTM SportClosed
      Open{} -> putTMVar r $ Rd n res
      _      -> retry
  atomically $ do
    result <- takeTMVar res
    case result of
      Left err -> throwSTM err
      Right bs -> return bs

-- | Read up to n bytes from the serial port. If the serial port
-- is closed then throw 'SportClosed'. Block if the serial port
-- is busy handling a concurrent request or until some of
-- the n bytes are available.
readSomeSport :: Sport -> Int -> IO ByteString
readSomeSport (Sport s r _) n = do
  res <- newEmptyTMVarIO
  atomically $ do
    st <- readTVar s
    case st of
      Closed -> throwSTM SportClosed
      Open{} -> putTMVar r $ RdSome n res
      _      -> retry
  atomically $ do
    result <- takeTMVar res
    case result of
      Left err -> throwSTM err
      Right bs -> return bs

-- | Write bytes to the serial port. If the serial port
-- is closed then throw 'SportClosed'. Block if the serial
-- port is busy handling a concurrent request.
writeSport :: Sport -> ByteString -> IO ()
writeSport (Sport s _ w) bs = do
  res <- newEmptyTMVarIO
  atomically $ do
    st <- readTVar s
    case st of
      Closed -> throwSTM SportClosed
      Open{} -> putTMVar w $ Wr bs res
      _      -> retry
  atomically $ do
    result <- takeTMVar res
    case result of
      Left err -> throwSTM err
      Right () -> return ()

-- | Acquire a serial port and run the daemon.
withSport :: (Sport -> IO a) -> IO a
withSport k = do
  s <- newSportIO
  either id id <$> race (k s) (runSport s)

-- | Run the serial port daemon and process requests.
runSport :: Sport -> IO a
runSport s =
  forever (runState s =<< readTVarIO (state s)) `onException` closeSport s

runState :: Sport -> State -> IO ()
runState s st = case st of
  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
    ]

readHandler :: Sport -> Handle -> IO a
readHandler s serial = forever $ do
  cmd <- atomically $ takeTMVar $ rcmd s
  case cmd of
    Rd n res -> handleException res $ do
      bs <- BS.hGet serial n
      atomically $ writeTMVar res $ Right bs
    RdSome n res -> handleException res $ do
      bs <- BS.fromStrict <$> Strict.hGetSome serial n
      atomically $ writeTMVar res $ Right bs

writeHandler :: Sport -> Handle -> IO a
writeHandler s serial = forever $ do
  cmd <- atomically $ takeTMVar $ wcmd s
  case cmd of
    Wr bs res -> handleException res $ do
      BS.hPut serial bs
      atomically $ writeTMVar res $ Right ()

waitNewState :: Sport -> State -> IO ()
waitNewState s st = atomically $ check . (st /=) =<< readTVar (state s)

opening :: Sport -> SportCfg -> TMVar (Either SomeException ()) -> IO ()
opening s cfg res =
  bracketOnError (S.openSerial $ toSerialCfg cfg) hClose $ \serial -> do
    hSetBinaryMode serial $ binaryMode cfg
    hSetBuffering serial $ bufferMode cfg
    atomically $ do
      writeTVar (state s) $ Open cfg serial
      writeTMVar res $ Right ()

toSerialCfg :: SportCfg -> S.SerialCfg
toSerialCfg s =
  S.SerialCfg
    (path s)
    (speed s)
    (byteSize s)
    (parity s)
    (stopBits s)
    (rtimeout s)
    (excl s)

handleException :: TMVar (Either SomeException a) -> IO () -> IO ()
handleException res k = k `catches`
  [ Handler $ \e -> throwIO (e :: SomeAsyncException)
  , Handler $ \e -> atomically $ void $ tryPutTMVar res $ Left (e :: SomeException)
  ]

-- | Serial port exceptions
data SportException
    -- | User required an open sport but it was closed.
  = SportClosed
    -- | User attempted to open a serial port that was already open.
  | 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