packages feed

pms-infra-serial-0.0.2.0: src/PMS/Infra/Serial/DS/Core.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

module PMS.Infra.Serial.DS.Core where

import System.IO
import Control.Monad.Logger
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Lens
import Control.Monad.Reader
import qualified Control.Concurrent as CC 
import Control.Concurrent.Async
import qualified Control.Concurrent.STM as STM
import Data.Conduit
import qualified Data.Text as T
import Control.Monad.Except
import qualified Control.Exception.Safe as E
import System.Exit
import qualified Data.Text.Encoding as TE
import Data.Aeson 
import qualified Data.ByteString.Char8 as BS8
import Data.Word 
import qualified Data.ByteString as B
import System.Hardware.Serialport

import qualified PMS.Domain.Model.DS.Utility as DM
import qualified PMS.Domain.Model.DM.Type as DM
import qualified PMS.Domain.Model.DM.Constant as DM

import PMS.Infra.Serial.DM.Type
import PMS.Infra.Serial.DS.Utility


-- |
--
app :: AppContext ()
app = do
  $logDebugS DM._LOGTAG "app called."
  runConduit pipeline
  where
    pipeline :: ConduitM () Void AppContext ()
    pipeline = src .| cmd2task .| sink

---------------------------------------------------------------------------------
-- |
--
src :: ConduitT () DM.SerialCommand AppContext ()
src = lift go >>= yield >> src
  where
    go :: AppContext DM.SerialCommand
    go = do
      queue <- view DM.serialQueueDomainData <$> lift ask
      dat <- liftIO $ STM.atomically $ STM.readTQueue queue
      return dat

---------------------------------------------------------------------------------
-- |
--
cmd2task :: ConduitT DM.SerialCommand (IOTask ()) AppContext ()
cmd2task = await >>= \case
  Just cmd -> flip catchError (errHdl cmd) $ do
    lift (go cmd) >>= yield >> cmd2task
  Nothing -> do
    $logWarnS DM._LOGTAG "cmd2task: await returns nothing. skip."
    cmd2task

  where
    errHdl :: DM.SerialCommand -> String -> ConduitT DM.SerialCommand (IOTask ()) AppContext ()
    errHdl serialCmd msg = do
      let jsonrpc = DM.getJsonRpcSerialCommand serialCmd
      $logWarnS DM._LOGTAG $ T.pack $ "cmd2task: exception occurred. skip. " ++ msg
      lift $ errorToolsCallResponse jsonrpc $ "cmd2task: exception occurred. skip. " ++ msg
      cmd2task

    go :: DM.SerialCommand -> AppContext (IOTask ())
    go (DM.SerialEchoCommand dat)    = genEchoTask dat
    go (DM.SerialOpenCommand dat)    = genSerialOpenTask dat
    go (DM.SerialCloseCommand dat)   = genSerialCloseTask dat
    go (DM.SerialReadCommand dat)    = genSerialReadTask dat
    go (DM.SerialWriteCommand dat)   = genSerialWriteTask dat
    go (DM.SerialMessageCommand dat) = genSerialMessageTask dat

---------------------------------------------------------------------------------
-- |
--
sink :: ConduitT (IOTask ()) Void AppContext ()
sink = await >>= \case
  Just req -> flip catchError errHdl $ do
    lift (go req) >> sink
  Nothing -> do
    $logWarnS DM._LOGTAG "sink: await returns nothing. skip."
    sink

  where
    errHdl :: String -> ConduitT (IOTask ()) Void AppContext ()
    errHdl msg = do
      $logWarnS DM._LOGTAG $ T.pack $ "sink: exception occurred. skip. " ++ msg
      sink

    go :: (IO ()) -> AppContext ()
    go task = do
      $logDebugS DM._LOGTAG "sink: start async."
      _ <- liftIOE $ async task
      $logDebugS DM._LOGTAG "sink: end async."
      return ()


---------------------------------------------------------------------------------
-- |
--
genEchoTask :: DM.SerialEchoCommandData -> AppContext (IOTask ())
genEchoTask dat = do
  resQ <- view DM.responseQueueDomainData <$> lift ask
  let val = dat^.DM.valueSerialEchoCommandData

  $logDebugS DM._LOGTAG $ T.pack $ "echoTask: echo : " ++ val
  return $ echoTask resQ dat val


-- |
--
echoTask :: STM.TQueue DM.McpResponse -> DM.SerialEchoCommandData -> String -> IOTask ()
echoTask resQ cmdDat val = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.echoTask run. " ++ val

  toolsCallResponse resQ (cmdDat^.DM.jsonrpcSerialEchoCommandData) ExitSuccess val ""

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.echoTask end."

  where
    errHdl :: E.SomeException -> IO ()
    errHdl e = toolsCallResponse resQ (cmdDat^.DM.jsonrpcSerialEchoCommandData) (ExitFailure 1) "" (show e)

-- |
--
genSerialOpenTask :: DM.SerialOpenCommandData -> AppContext (IOTask ())
genSerialOpenTask cmdDat = do
  let argsBS   = DM.unRawJsonByteString $ cmdDat^.DM.argumentsSerialOpenCommandData

  resQ <- view DM.responseQueueDomainData <$> lift ask
  serialMVar <- view handleAppData <$> ask
  prompts <- view DM.promptsDomainData <$> lift ask
  lockTMVar <- view lockAppData <$> ask
  argsDat <- liftEither $ eitherDecode $ argsBS


  let device = argsDat^.deviceSerialToolParams
      speed  = argsDat^.speedSerialToolParams

  settings <- getSerialSetting speed
  $logDebugS DM._LOGTAG $ T.pack $ "genSerialOpenTask: cmd. " ++ device ++ ":" ++ show settings
 
  return $ serialOpenTask cmdDat resQ serialMVar lockTMVar prompts device settings


-- |
--   
serialOpenTask :: DM.SerialOpenCommandData
              -> STM.TQueue DM.McpResponse
              -> STM.TMVar (Maybe Handle)
              -> STM.TMVar ()  -- lock
              -> [String]      -- prompt list
              -> String        -- device
              -> SerialPortSettings
              -> IOTask ()
serialOpenTask cmdDat resQ serialVar lockTMVar prompts device settings = do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialOpenTask start. "
  let waitmicro = 100000
      readsize = 1024
      tout = DM._TIMEOUT_MICROSEC
      promptAdded = prompts ++ ["login:"]
      jsonRpc = cmdDat^.DM.jsonrpcSerialOpenCommandData

  STM.atomically (STM.takeTMVar serialVar) >>= \case
    Just p -> do
      STM.atomically $ STM.putTMVar serialVar $ Just p
      hPutStrLn stderr "[ERROR] PMS.Infrastructure.DS.Core.serialOpenTask: serial is already connected."
      toolsCallResponse resQ (cmdDat^.DM.jsonrpcSerialOpenCommandData) (ExitFailure 1) "" "serial is already running."
    Nothing -> flip E.catchAny errHdl $ do
      hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialOpenTask end." ++ (show settings)
      serialhdl <- createSerialPort device settings
      writeSerialPort serialhdl $ BS8.pack $ DM._CR
      CC.threadDelay waitmicro

      STM.atomically $ STM.putTMVar serialVar (Just serialhdl)
      hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialOpenTask end. serial connected to " ++ device ++ ":" ++ show settings

  STM.atomically (STM.readTMVar serialVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Serial.DS.Core.serialOpenTask: serial is not started."
      toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not started."
    Just serialhdl -> do
      race (DM.expect lockTMVar (readSizeSerialPort serialhdl readsize) promptAdded) (CC.threadDelay tout) >>= \case
        Left res -> toolsCallResponse resQ jsonRpc ExitSuccess (maybe "Nothing" id res) ""
        Right _  -> toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "timeout occurred."

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.serialOpenTask end."

  where
    errHdl :: E.SomeException -> IO ()
    errHdl e = do
      STM.atomically $ STM.putTMVar serialVar Nothing
      hPutStrLn stderr $ "[ERROR] PMS.Infra.Serial.DS.Core.serialRunTask: exception occurred. " ++ show e
      toolsCallResponse resQ (cmdDat^.DM.jsonrpcSerialOpenCommandData) (ExitFailure 1) "" (show e)

-- |
--
genSerialCloseTask :: DM.SerialCloseCommandData -> AppContext (IOTask ())
genSerialCloseTask dat = do
  $logDebugS DM._LOGTAG $ T.pack $ "genSerialCloseTask called. "
  serialTMVar <- view handleAppData <$> ask
  resQ <- view DM.responseQueueDomainData <$> lift ask
  return $ serialCloseTask dat resQ serialTMVar

-- |
--
serialCloseTask :: DM.SerialCloseCommandData
                  -> STM.TQueue DM.McpResponse
                  -> STM.TMVar (Maybe Handle)
                  -> IOTask ()
serialCloseTask cmdDat resQ serialTMVar = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialCloseTask run. "
  let jsonRpc = cmdDat^.DM.jsonrpcSerialCloseCommandData

  STM.atomically (STM.swapTMVar serialTMVar Nothing) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Serial.DS.Core.serialCloseTask: serial is not started."
      toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not started."
    Just serialhdl -> do
      hClose serialhdl
      toolsCallResponse resQ jsonRpc ExitSuccess "" "serial is teminated."
      hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialCloseTask closeSerial : "

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.serialCloseTask end."

  where
    -- |
    --
    errHdl :: E.SomeException -> IO ()
    errHdl e = toolsCallResponse resQ (cmdDat^.DM.jsonrpcSerialCloseCommandData) (ExitFailure 1) "" (show e)


-- |
--
genSerialReadTask :: DM.SerialReadCommandData -> AppContext (IOTask ())
genSerialReadTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsSerialReadCommandData
      tout = 30 * 1000 * 1000
  resQ <- view DM.responseQueueDomainData <$> lift ask
  serialTMVar <- view handleAppData <$> ask
  argsDat <- liftEither $ eitherDecode $ argsBS
  let size = argsDat^.sizeSerialIntToolParams

  $logDebugS DM._LOGTAG $ T.pack $ "genSerialReadTask: args. " ++ show size
  return $ serialReadTask cmdData resQ serialTMVar size tout

-- |
--
serialReadTask :: DM.SerialReadCommandData
                -> STM.TQueue DM.McpResponse
                -> STM.TMVar (Maybe Handle)
                -> Int       -- read size.
                -> Int       -- timeout microsec
                -> IOTask ()
serialReadTask cmdDat resQ serialTMVar size tout = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialReadTask run. " ++ show size
    
  STM.atomically (STM.readTMVar serialTMVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Serial.DS.Core.serialReadTask: serial is not started."
      toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not started."
    Just p -> go p

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.serialReadTask end."

  where
    jsonRpc :: DM.JsonRpcRequest
    jsonRpc = cmdDat^.DM.jsonrpcSerialReadCommandData

    errHdl :: E.SomeException -> IO ()
    errHdl e = toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)

    go :: Handle -> IO ()
    go serialhdl =
      race (readSizeSerialPort serialhdl size) (CC.threadDelay tout) >>= \case
        Left res -> toolsCallResponse resQ jsonRpc ExitSuccess (bytesToHex res) ""
        Right _  -> toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "timeout occurred."


-- |
--
genSerialWriteTask :: DM.SerialWriteCommandData -> AppContext (IOTask ())
genSerialWriteTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsSerialWriteCommandData
  resQ <- view DM.responseQueueDomainData <$> lift ask
  serialTMVar <- view handleAppData <$> ask
  argsDat <- liftEither $ eitherDecode $ argsBS
  let args = argsDat^.dataSerialWord8ArrayToolParams

  $logDebugS DM._LOGTAG $ T.pack $ "genSerialWriteTask: args. " ++ show args
  return $ serialWriteTask cmdData resQ serialTMVar args

-- |
--
serialWriteTask :: DM.SerialWriteCommandData
                -> STM.TQueue DM.McpResponse
                -> STM.TMVar (Maybe Handle)
                -> [Word8]
                -> IOTask ()
serialWriteTask cmdDat resQ serialTMVar args = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialWriteTask run. " ++ show args
    
  STM.atomically (STM.readTMVar serialTMVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Serial.DS.Core.serialWriteTask: serial is not started."
      toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not started."
    Just p -> go p

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.serialWriteTask end."

  where
    jsonRpc :: DM.JsonRpcRequest
    jsonRpc = cmdDat^.DM.jsonrpcSerialWriteCommandData

    errHdl :: E.SomeException -> IO ()
    errHdl e = toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)

    go :: Handle -> IO ()
    go serialhdl = do
      let bsDat = B.pack args

      hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialWriteTask writeSerial (hex): " ++ bytesToHex bsDat

      writeSerialPort serialhdl bsDat

      toolsCallResponse resQ jsonRpc ExitSuccess ("write data to serial. "++ bytesToHex bsDat) ""


-- |
--
genSerialMessageTask :: DM.SerialMessageCommandData -> AppContext (IOTask ())
genSerialMessageTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsSerialMessageCommandData
      tout = DM._TIMEOUT_MICROSEC
  prompts <- view DM.promptsDomainData <$> lift ask
  resQ <- view DM.responseQueueDomainData <$> lift ask
  serialTMVar <- view handleAppData <$> ask
  lockTMVar <- view lockAppData <$> ask
  argsDat <- liftEither $ eitherDecode $ argsBS
  let args = argsDat^.argumentsSerialStringToolParams

  $logDebugS DM._LOGTAG $ T.pack $ "genSerialMessageTask: args. " ++ args
  return $ serialMessageTask cmdData resQ serialTMVar lockTMVar args prompts tout

-- |
--
serialMessageTask :: DM.SerialMessageCommandData
                -> STM.TQueue DM.McpResponse
                -> STM.TMVar (Maybe Handle)
                -> STM.TMVar ()
                -> String  -- arguments line
                -> [String]  -- prompt list
                -> Int       -- timeout microsec
                -> IOTask ()
serialMessageTask cmdDat resQ serialTMVar lockTMVar args prompts tout = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialMessageTask run. " ++ args
    
  STM.atomically (STM.readTMVar serialTMVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Serial.DS.Core.serialMessageTask: serial is not started."
      toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not started."
    Just p -> go p

  hPutStrLn stderr "[INFO] PMS.Infra.Serial.DS.Core.serialMessageTask end."

  where
    jsonRpc :: DM.JsonRpcRequest
    jsonRpc = cmdDat^.DM.jsonrpcSerialMessageCommandData

    errHdl :: E.SomeException -> IO ()
    errHdl e = toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)

    go :: Handle -> IO ()
    go serialhdl = do

      msg <- DM.validateMessage args
      let cmd = TE.encodeUtf8 $ T.pack $ msg ++ DM._CR
          waitmicro = 100000
          size = 1024
      hPutStrLn stderr $ "[INFO] PMS.Infra.Serial.DS.Core.serialMessageTask writeSerial : " ++ BS8.unpack cmd

      writeSerialPort serialhdl cmd
      CC.threadDelay waitmicro
      race (DM.expect lockTMVar (readSizeSerialPort serialhdl size) prompts) (CC.threadDelay tout) >>= \case
        Left res -> toolsCallResponse resQ jsonRpc ExitSuccess (maybe "Nothing" id res) ""
        Right _  -> toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "timeout occurred."