paprika-0.1.0.0: app/WaiCtrl.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
module Main where
import Data.String (fromString)
import System.Environment (getArgs)
import qualified Network.Wai.Handler.Warp as Warp
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types as H
import qualified Network.Wai.Application.Static as Static
import Data.Maybe (fromJust)
import Data.FileEmbed (embedDir)
import WaiAppStatic.Types (toPieces)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import qualified Data.ByteString as BS
import Network.Transport.InMemory (createTransport)
import Control.Distributed.Process.Node (LocalNode,newLocalNode,initRemoteTable,forkProcess,runProcess)
import Control.Distributed.Process (Process,ProcessId,send,receiveWait,match,getSelfPid,terminate)
import Control.Monad (forever,mzero)
import Control.Monad.Trans (liftIO)
import Control.Concurrent (threadDelay)
import Control.Exception (catch)
import qualified Data.Set as Set
import Network.Wai.Handler.WebSockets (websocketsOr)
import qualified Network.WebSockets as WS
import Data.Word8 (_question)
import Data.Maybe(mapMaybe)
import GHC.Generics (Generic)
import Data.Typeable (Typeable)
import Data.Binary (Binary)
import Data.Aeson.Types (ToJSON,toJSON,object,(.=),FromJSON,(.:))
import qualified Data.Aeson as AE
import qualified System.Paprika as PP
main :: IO ()
main = do
PP.stop
PP.setLeftArm 0
PP.setRightArm 0
node <- createTransport >>= (\t -> newLocalNode t initRemoteTable)
cmpid <- forkProcess node $ ctrlManagerProcess (Set.empty,CtrlCommandStop,(0,0))
host:port:_ <- getArgs
Warp.runSettings (
Warp.setHost (fromString host) $
Warp.setPort (read port) $
Warp.defaultSettings
) $ websocketsOr WS.defaultConnectionOptions (wsRouterApp node cmpid) staticApp
staticApp :: Wai.Application
staticApp = Static.staticApp $ settings { Static.ssIndices = indices }
where
-- settings = Static.defaultWebAppSettings "static/ctrl"
settings = Static.embeddedSettings $(embedDir "static/ctrl")
indices = fromJust $ toPieces ["default.htm"] -- default content
wsRouterApp :: LocalNode -> ProcessId -> WS.ServerApp
wsRouterApp node cmpid pconn
| ("/default" == path) = ctrlApp node cmpid pconn
| otherwise = WS.rejectRequest pconn "endpoint not found"
where
requestPath = WS.requestPath $ WS.pendingRequest pconn
path = BS.takeWhile (/=_question) requestPath
ctrlApp :: LocalNode -> ProcessId -> WS.ServerApp
ctrlApp node cmpid pconn = do
conn <- WS.acceptRequest pconn
WS.forkPingThread conn 30
cpid <- forkProcess node $ ctrlProcess conn
runProcess node $ do
send cmpid (CMMRegistCtrl cpid)
send cmpid (CMMQueryCommand cpid)
loop conn cpid `catch` onError cpid
where
loop :: WS.Connection -> ProcessId -> IO ()
loop conn cpid = do
msg <- WS.receive conn
case msg of
WS.ControlMessage (WS.Close _ _) -> onClose cpid
WS.DataMessage (WS.Text lbs) -> do
case AE.decode lbs :: Maybe CtrlManagerMsg of
Nothing -> return ()
Just cmd -> runProcess node $ send cmpid cmd
loop conn cpid
_ -> loop conn cpid
onError :: ProcessId -> WS.ConnectionException -> IO ()
onError cpid _ = onClose cpid
onClose cpid =
runProcess node $ do
send cmpid (CMMUnregistCtrl cpid)
send cpid CMClose
requestPath = WS.requestPath $ WS.pendingRequest pconn
query = BS.drop 1 $ BS.dropWhile (/=_question) requestPath
name = T.unpack $ decodeUtf8 $ H.urlDecode True query
data CtrlCommand = CtrlCommandStop
| CtrlCommandForward
| CtrlCommandBackward
| CtrlCommandForwardLeft
| CtrlCommandForwardRight
| CtrlCommandBackwardLeft
| CtrlCommandBackwardRight
| CtrlCommandTurnLeft
| CtrlCommandTurnRight
| CtrlCommandLeftArm PP.ArmLevel
| CtrlCommandRightArm PP.ArmLevel
deriving (Generic,Typeable)
instance Show CtrlCommand where
show CtrlCommandStop = "s"
show CtrlCommandForward = "f"
show CtrlCommandBackward = "b"
show CtrlCommandForwardLeft = "fl"
show CtrlCommandForwardRight = "fr"
show CtrlCommandBackwardLeft = "bl"
show CtrlCommandBackwardRight = "br"
show CtrlCommandTurnLeft = "tl"
show CtrlCommandTurnRight = "tr"
show (CtrlCommandLeftArm _) = "al"
show (CtrlCommandRightArm _) = "ar"
instance Read CtrlCommand where
readsPrec _ = mapMaybe t' <$> lex
where
t' :: (String,String) -> Maybe (CtrlCommand,String)
t' (p,r) = t p >>= (\p' -> return (p',r))
t :: String -> Maybe CtrlCommand
t s = case s of
"s" -> Just CtrlCommandStop
"f" -> Just CtrlCommandForward
"b" -> Just CtrlCommandBackward
"fl" -> Just CtrlCommandForwardLeft
"fr" -> Just CtrlCommandForwardRight
"bl" -> Just CtrlCommandBackwardLeft
"br" -> Just CtrlCommandBackwardRight
"tl" -> Just CtrlCommandTurnLeft
"tr" -> Just CtrlCommandTurnRight
_ -> Nothing
instance Binary CtrlCommand
instance FromJSON CtrlCommand where
parseJSON (AE.String s) = return $ read $ T.unpack s
parseJSON (AE.Object o) = do
cmd <- T.unpack <$> o .: "name"
case cmd of
"al" -> CtrlCommandLeftArm <$> o .: "level"
"ar" -> CtrlCommandRightArm <$> o .: "level"
parseJSON _ = mzero
type CtrlManagerState = (Set.Set ProcessId,CtrlCommand,(PP.ArmLevel,PP.ArmLevel))
data CtrlManagerMsg = CMMRegistCtrl ProcessId
| CMMUnregistCtrl ProcessId
| CMMSetCommand CtrlCommand
| CMMQueryCommand ProcessId
deriving (Show,Generic,Typeable)
instance Binary CtrlManagerMsg
instance FromJSON CtrlManagerMsg where
parseJSON v@(AE.Object o) = do
cmd <- T.unpack <$> o .: "name"
case cmd of
"al" -> CMMSetCommand <$> AE.parseJSON v
"ar" -> CMMSetCommand <$> AE.parseJSON v
otherwise -> CMMSetCommand <$> o .: "name"
parseJSON _ = mzero
ctrlManagerProcess :: CtrlManagerState -> Process ()
ctrlManagerProcess state = do
state' <- receiveWait [match (p state)]
ctrlManagerProcess state'
where
p :: CtrlManagerState -> CtrlManagerMsg -> Process CtrlManagerState
p (cs,cmd,arm) (CMMRegistCtrl cpid) = return $ (Set.insert cpid cs,cmd,arm)
p (cs,cmd,arm) (CMMUnregistCtrl cpid) = return $ (Set.delete cpid cs,cmd,arm)
p (cs,cmd,(_,ar)) (CMMSetCommand cmd'@(CtrlCommandLeftArm lev)) = do
liftIO $ PP.setLeftArm lev
mapM_ (\pid -> send pid $ CMCommand cmd') $ Set.toList cs
return $ (cs,cmd,(lev,ar))
p (cs,cmd,(al,_)) (CMMSetCommand cmd'@(CtrlCommandRightArm lev)) = do
liftIO $ PP.setRightArm lev
mapM_ (\pid -> send pid $ CMCommand cmd') $ Set.toList cs
return $ (cs,cmd,(al,lev))
p (cs,_,arm) (CMMSetCommand cmd) = do
case cmd of
CtrlCommandStop -> liftIO PP.stop
CtrlCommandForward -> liftIO PP.forward
CtrlCommandBackward -> liftIO PP.backward
CtrlCommandForwardLeft -> liftIO PP.forwardLeft
CtrlCommandForwardRight -> liftIO PP.forwardRight
CtrlCommandBackwardLeft -> liftIO PP.backwardLeft
CtrlCommandBackwardRight -> liftIO PP.backwardRight
CtrlCommandTurnLeft -> liftIO PP.turnLeft
CtrlCommandTurnRight -> liftIO PP.turnRight
mapM_ (\pid -> send pid $ CMCommand cmd) $ Set.toList cs
return $ (cs,cmd,arm)
p state@(cs,cmd,(al,ar)) (CMMQueryCommand cpid) = do
send cpid $ CMCommand cmd
send cpid $ CMCommand $ CtrlCommandLeftArm al
send cpid $ CMCommand $ CtrlCommandRightArm ar
return state
data CtrlMsg = CMCommand CtrlCommand
| CMClose
deriving (Show,Generic,Typeable)
instance Binary CtrlMsg
instance ToJSON CtrlMsg where
toJSON (CMCommand cmd@(CtrlCommandLeftArm lev)) =
object ["type" .= ("command" :: String), "name" .= show cmd, "level" .= lev]
toJSON (CMCommand cmd@(CtrlCommandRightArm lev)) =
object ["type" .= ("command" :: String), "name" .= show cmd, "level" .= lev ]
toJSON (CMCommand cmd) =
object ["type" .= ("command" :: String), "name" .= show cmd ]
ctrlProcess :: WS.Connection -> Process ()
ctrlProcess conn = forever $ receiveWait [match (p conn)]
where
p :: WS.Connection -> CtrlMsg -> Process ()
p conn msg@(CMCommand _ ) = liftIO $ WS.sendTextData conn $ AE.encode msg
p conn CMClose = terminate