packages feed

pms-infra-agent-serial-0.0.1.0: src/PMS/Infra/Agent/Serial/DS/Core.hs

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

module PMS.Infra.Agent.Serial.DS.Core where

import System.IO
import System.Hardware.Serialport
import Control.Monad.Logger
import Control.Monad.IO.Class
import Control.Monad.Trans.Class
import Control.Lens
import Control.Monad.Reader
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 Data.Aeson

import qualified Data.ByteString as BS
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.Agent.Serial.DM.Type
import PMS.Infra.Agent.Serial.DM.Constant
import PMS.Infra.Agent.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.AgentSerialCommand AppContext ()
src = lift go >>= yield >> src
  where
    go :: AppContext DM.AgentSerialCommand
    go = do
      queue <- view DM.agentSerialQueueDomainData <$> lift ask
      dat <- liftIO $ STM.atomically $ STM.readTQueue queue
      return dat

---------------------------------------------------------------------------------
-- |
--
cmd2task :: ConduitT DM.AgentSerialCommand (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.AgentSerialCommand -> String -> ConduitT DM.AgentSerialCommand (IOTask ()) AppContext ()
    errHdl serialCmd msg = do
      let jsonrpc = DM.getJsonRpcAgentSerialCommand serialCmd
      $logWarnS DM._LOGTAG $ T.pack $ "cmd2task: exception occurred. skip. " ++ msg
      lift $ errorToolsCallResponse jsonrpc $ "cmd2task: exception occurred. skip. " ++ msg
      cmd2task

    go :: DM.AgentSerialCommand -> AppContext (IOTask ())
    go (DM.AgentSerialEchoCommand      dat) = genEchoTask dat
    go (DM.AgentSerialOpenCommand      dat) = genSerialOpenTask dat
    go (DM.AgentSerialCloseCommand     dat) = genSerialCloseTask dat
    go (DM.AgentSerialReadCommand      dat) = genSerialReadTask dat
    go (DM.AgentSerialWriteCommand     dat) = genSerialWriteTask dat
    go (DM.AgentSerialReadByteCommand  dat) = genSerialReadByteTask dat
    go (DM.AgentSerialWriteByteCommand dat) = genSerialWriteByteTask 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 ()


---------------------------------------------------------------------------------
-- |
--
notImplemented :: String -> DM.JsonRpcRequest -> AppContext (IOTask ())
notImplemented name jsonRpc = do
  resQ <- view DM.responseQueueDomainData <$> lift ask
  return $ DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (name ++ ": not implemented yet.")

---------------------------------------------------------------------------------
-- |
--
genEchoTask :: DM.AgentSerialEchoCommandData -> AppContext (IOTask ())
genEchoTask dat = do
  resQ <- view DM.responseQueueDomainData <$> lift ask
  let val = dat^.DM.valueAgentSerialEchoCommandData
  $logDebugS DM._LOGTAG $ T.pack $ "genEchoTask: echo : " ++ val
  return $ echoTask resQ dat val

-- |
--
echoTask :: STM.TQueue DM.McpResponse -> DM.AgentSerialEchoCommandData -> String -> IOTask ()
echoTask resQ cmdDat val = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Agent.Serial.DS.Core.echoTask run. " ++ val
  DM.toolsCallResponse resQ (cmdDat^.DM.jsonrpcAgentSerialEchoCommandData) ExitSuccess val ""
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.echoTask end."
  where
    errHdl :: E.SomeException -> IO ()
    errHdl e = DM.toolsCallResponse resQ (cmdDat^.DM.jsonrpcAgentSerialEchoCommandData) (ExitFailure 1) "" (show e)

---------------------------------------------------------------------------------
-- |
--
genSerialOpenTask :: DM.AgentSerialOpenCommandData -> AppContext (IOTask ())
genSerialOpenTask cmdDat = do
  let argsBS = DM.unRawJsonByteString $ cmdDat^.DM.argumentsAgentSerialOpenCommandData
  resQ        <- view DM.responseQueueDomainData <$> lift ask
  serialPortTMVar <- view serialPortAppData <$> ask
  argsDat     <- liftEither $ eitherDecode argsBS
  settings    <- getSerialSetting argsDat
  let device = argsDat^.deviceSerialToolParams
  return $ serialOpenTask cmdDat resQ serialPortTMVar device settings

-- |
--
serialOpenTask :: DM.AgentSerialOpenCommandData
               -> STM.TQueue DM.McpResponse
               -> STM.TMVar (Maybe SerialPort)
               -> String
               -> SerialPortSettings
               -> IOTask ()
serialOpenTask cmdDat resQ serialPortTMVar device settings = flip E.catchAny errHdl $ do
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialOpenTask start."
  STM.atomically (STM.takeTMVar serialPortTMVar) >>= \case
    Just sp -> do
      STM.atomically $ STM.putTMVar serialPortTMVar (Just sp)
      hPutStrLn stderr "[ERROR] serialOpenTask: serial is already connected."
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is already connected."
    Nothing -> flip E.catchAny connErrHdl $ do
      sp <- openSerial device settings
      STM.atomically $ STM.putTMVar serialPortTMVar (Just sp)
      DM.toolsCallResponse resQ jsonRpc ExitSuccess ("serial connected to " ++ device) ""
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialOpenTask end."
  where
    jsonRpc = cmdDat^.DM.jsonrpcAgentSerialOpenCommandData
    errHdl :: E.SomeException -> IO ()
    errHdl e = DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)
    connErrHdl :: E.SomeException -> IO ()
    connErrHdl e = do
      STM.atomically $ STM.putTMVar serialPortTMVar Nothing
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)

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

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

  STM.atomically (STM.swapTMVar serialPortTMVar Nothing) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Agent.Serial.DS.Core.serialCloseTask: serial is not connected."
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not connected."
    Just sp -> do
      closeSerial sp
      DM.toolsCallResponse resQ jsonRpc ExitSuccess "" "serial is closed."
      hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialCloseTask closeSerial."

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

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

---------------------------------------------------------------------------------
-- |
--
genSerialReadTask :: DM.AgentSerialReadCommandData -> AppContext (IOTask ())
genSerialReadTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsAgentSerialReadCommandData
  resQ        <- view DM.responseQueueDomainData <$> lift ask
  serialPortTMVar <- view serialPortAppData <$> ask
  argsDat     <- liftEither $ eitherDecode argsBS
  let size       = argsDat^.sizeSerialIntToolParams
      actualSize = min size _READ_BUFFER_SIZE
  return $ serialReadTask cmdData resQ serialPortTMVar actualSize

-- |
--
serialReadTask :: DM.AgentSerialReadCommandData
               -> STM.TQueue DM.McpResponse
               -> STM.TMVar (Maybe SerialPort)
               -> Int
               -> IOTask ()
serialReadTask cmdDat resQ serialPortTMVar size = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialReadTask run. " ++ show size

  STM.atomically (STM.readTMVar serialPortTMVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Agent.Serial.DS.Core.serialReadTask: serial is not connected."
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not connected."
    Just sp -> do
      output <- recv sp size
      DM.toolsCallResponse resQ jsonRpc ExitSuccess (DM.bs2strUTF8 output) ""

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

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

---------------------------------------------------------------------------------
-- |
--
genSerialReadByteTask :: DM.AgentSerialReadByteCommandData -> AppContext (IOTask ())
genSerialReadByteTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsAgentSerialReadByteCommandData
  resQ        <- view DM.responseQueueDomainData <$> lift ask
  serialPortTMVar <- view serialPortAppData <$> ask
  argsDat     <- liftEither $ eitherDecode argsBS
  let size       = argsDat^.sizeSerialIntToolParams
      actualSize = min size _READ_BUFFER_SIZE
  return $ serialReadByteTask cmdData resQ serialPortTMVar actualSize

-- |
--
serialReadByteTask :: DM.AgentSerialReadByteCommandData
                   -> STM.TQueue DM.McpResponse
                   -> STM.TMVar (Maybe SerialPort)
                   -> Int
                   -> IOTask ()
serialReadByteTask cmdDat resQ serialPortTMVar size = flip E.catchAny errHdl $ do
  hPutStrLn stderr $ "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialReadByteTask run. " ++ show size

  STM.atomically (STM.readTMVar serialPortTMVar) >>= \case
    Nothing -> do
      hPutStrLn stderr "[ERROR] PMS.Infra.Agent.Serial.DS.Core.serialReadByteTask: serial is not connected."
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not connected."
    Just sp -> do
      output <- recv sp size
      DM.toolsCallResponse resQ jsonRpc ExitSuccess (DM.bytesToHex output) ""

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

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



---------------------------------------------------------------------------------
-- | CR-07 / invalidPatterns check: agent-serial-write implementation.
-- The invalidPatterns check is performed here in the gen function (AppContext),
-- consistent with how genSocketWriteTask performs the same check.
genSerialWriteTask :: DM.AgentSerialWriteCommandData -> AppContext (IOTask ())
genSerialWriteTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsAgentSerialWriteCommandData
  resQ            <- view DM.responseQueueDomainData <$> lift ask
  serialPortTMVar <- view serialPortAppData <$> ask
  invalidPatterns <- view DM.invalidPatternsDomainData <$> lift ask
  argsDat         <- liftEither $ eitherDecode argsBS
  let str           = argsDat^.dataSerialWriteToolParams
      appendNewline = argsDat^.appendNewlineSerialWriteToolParams
  -- invalidPatterns check (pure check, consistent with eitherDecode validation)
  case DM.checkInvalidPatterns invalidPatterns str of
    Just pat -> throwError $
      "agent-serial-write: rejected. input matches invalidPatterns: " ++ show pat
    Nothing  -> return ()
  return $ serialWriteTask cmdData resQ serialPortTMVar str appendNewline

-- |
--
serialWriteTask :: DM.AgentSerialWriteCommandData
                -> STM.TQueue DM.McpResponse
                -> STM.TMVar (Maybe SerialPort)
                -> String
                -> Maybe Bool
                -> IOTask ()
serialWriteTask cmdDat resQ serialPortTMVar str appendNewline = flip E.catchAny errHdl $ do
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialWriteTask run."

  STM.atomically (STM.readTMVar serialPortTMVar) >>= \case
    Nothing ->
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not connected."
    Just sp -> do
      -- Apply appendCRLF (auto-append newline) unless appendNewline is explicitly False.
      let payload = if appendNewline == Just False
                      then str
                      else DM.appendCRLF str
          bs = DM.str2bsUTF8 payload
      hPutStrLn stderr $ "PMS.Infra.Agent.Serial.DS.Core.serialWriteTask " ++ show bs
      _ <- send sp bs
      DM.toolsCallResponse resQ jsonRpc ExitSuccess "" ""

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

  where
    jsonRpc = cmdDat^.DM.jsonrpcAgentSerialWriteCommandData
    errHdl e = DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)


---------------------------------------------------------------------------------
-- |
--
genSerialWriteByteTask :: DM.AgentSerialWriteByteCommandData -> AppContext (IOTask ())
genSerialWriteByteTask cmdData = do
  let argsBS = DM.unRawJsonByteString $ cmdData^.DM.argumentsAgentSerialWriteByteCommandData
  resQ        <- view DM.responseQueueDomainData <$> lift ask
  serialPortTMVar <- view serialPortAppData <$> ask
  argsDat     <- liftEither $ eitherDecode argsBS
  let hex = argsDat^.dataSerialHexToolParams
  bs          <- liftEither $ hexToBytes hex
  return $ serialWriteByteTask cmdData resQ serialPortTMVar bs

-- |
--
serialWriteByteTask :: DM.AgentSerialWriteByteCommandData
                    -> STM.TQueue DM.McpResponse
                    -> STM.TMVar (Maybe SerialPort)
                    -> BS.ByteString
                    -> IOTask ()
serialWriteByteTask cmdDat resQ serialPortTMVar bs = flip E.catchAny errHdl $ do
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Serial.DS.Core.serialWriteByteTask run."

  STM.atomically (STM.readTMVar serialPortTMVar) >>= \case
    Nothing ->
      DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" "serial is not connected."
    Just sp -> do
      hPutStrLn stderr $ "PMS.Infra.Agent.Serial.DS.Core.serialWriteByteTask " ++ show bs
      _ <- send sp bs
      DM.toolsCallResponse resQ jsonRpc ExitSuccess "" ""

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

  where
    jsonRpc = cmdDat^.DM.jsonrpcAgentSerialWriteByteCommandData
    errHdl e = DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" (show e)