packages feed

sport-3.0.0.0: lib/Sport/Sport.hs

-- | Internal high-level serial port
module Sport.Sport
  ( Sport
  , withNewSport
  , newSportIO
  , newSport
  , withSport
  , openSport
  , isOpenSport
  , getSportCfg
  , defSportCfg
  , SportCfg(..)
  , closeSport
  , readSport
  , readSomeSport
  , writeSport
  , flushSport
  , SportException(..)
  , displaySportException
  ) where

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 qualified Sport.Serial as S
import System.IO
import System.Posix

-- | Serial port handle
data Sport = Sport
  { cfgVar  :: TMVar SportCfg
  , hndlVar :: TMVar Handle
  }

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

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

-- | Check if serial port is open
isOpenSport :: Sport -> STM Bool
isOpenSport = fmap not . isEmptyTMVar . cfgVar

-- | 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)

-- | Acquire new handle 'newSportIO' then bracket
-- 'openSport' with a configuration to 'closeSport'.
withNewSport :: SportCfg -> (Sport -> IO a) -> IO a
withNewSport cfg k = do
  s <- newSportIO
  withSport s cfg $ k s

-- | Bracket 'openSport' and 'closeSport'.
withSport :: Sport -> SportCfg -> IO a -> IO a
withSport s cfg = bracket_ (openSport s cfg) (closeSport s)

-- | Open the serial port. Throw 'SportAlreadyOpen' if the
-- serial port was already opened.
openSport :: Sport -> SportCfg -> IO ()
openSport s cfg' =
  bracketOnError (S.openSerial $ toSerialCfg cfg') hClose $ \serial -> do
    hSetBinaryMode serial $ binaryMode cfg'
    hSetBuffering  serial $ bufferMode cfg'
    atomically $ do
      cfgM <- getSportCfg s
      case cfgM of
        Just cfg -> throwSTM $ SportAlreadyOpen $ path cfg
        Nothing  -> do
          putTMVar (cfgVar  s) cfg'
          putTMVar (hndlVar s) serial

-- | Get current configuration. Returns 'Just' if
-- the current state is open.
getSportCfg :: Sport -> STM (Maybe SportCfg)
getSportCfg = tryReadTMVar . cfgVar

-- | Close the serial port.
closeSport :: Sport -> IO ()
closeSport s = join $ atomically $ do
  _  <- tryTakeTMVar $ cfgVar  s
  hM <- tryTakeTMVar $ hndlVar s
  return $ mapM_ hClose hM

-- | Read n bytes from the serial port. If the serial port is
-- closed then throw 'SportClosed'. Blocks until all n bytes
-- are available.
readSport :: Sport -> Int -> IO ByteString
readSport s n = do
  h <- atomically $ readTMVar (hndlVar s) `orElse` throwSTM SportClosed
  BS.hGet h n

-- | Read up to n bytes from the serial port. If the serial port
-- is closed then throw 'SportClosed'. Blocks until some of
-- the n bytes are available.
readSomeSport :: Sport -> Int -> IO ByteString
readSomeSport s n = do
  h <- atomically $ readTMVar (hndlVar s) `orElse` throwSTM SportClosed
  BS.fromStrict <$> Strict.hGetSome h n

-- | 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 s bs = do
  h <- atomically $ readTMVar (hndlVar s) `orElse` throwSTM SportClosed
  BS.hPut h bs

-- | Flush the serial handle. Do nothing if closed.
flushSport :: Sport -> IO ()
flushSport s = do
  hM <- atomically $ tryReadTMVar $ hndlVar s
  mapM_ hFlush hM

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

-- | 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, Read, Show)

instance Exception SportException

-- | Pretty exception rendering
displaySportException :: SportException -> String
displaySportException e = "sport exception: " <> case e of
  SportClosed -> "serial port closed"
  SportAlreadyOpen p -> "serial port already open: " <> p