packages feed

pinpon-0.2.0.1: pinpon-gpio/Main.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Protolude hiding (option)
import Control.Concurrent (threadDelay)
import Control.Lens ((^.))
import Control.Monad (forever, unless, void)
import Control.Monad.Catch.Pure (runCatch)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.ByteString.Char8 as C8 (unpack)
import Data.Monoid ((<>))
import Data.String (String)
import Data.Text (Text, pack)
import qualified Data.Text as T (unwords)
import qualified Data.Text.IO as T (putStrLn, hPutStrLn)
import Data.Time.Clock
       (NominalDiffTime, diffUTCTime, getCurrentTime)
import Network.HTTP.Client (newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Types (Status(..))
import Network.PinPon.Client
       (Notification(..), defaultNotification, headline, message, notify,
        sound)
import Options.Applicative hiding (action)
import Options.Applicative.Text (text)
import Servant.Client
       (BaseUrl, ClientEnv(..), ServantError(..), parseBaseUrl,
        runClientM)
import System.GPIO.Linux.Sysfs (SysfsGpioIO, runSysfsGpioIO)
import System.GPIO.Monad
       (Pin(..), PinActiveLevel(..), PinInputMode(InputDefault),
        PinInterruptMode(..), withInterruptPin, pollInterruptPin)
import System.IO (stderr)

-- Only one for now.
data Interpreter =
  SysfsIO
  deriving (Eq,Show,Read)

data Options = Options
  { _quiet :: !Bool
  , _interpreter :: !Interpreter
  , _edge :: !PinInterruptMode
  , _activeLow :: !PinActiveLevel
  , _debounce :: !Int
  , _headline :: !Text
  , _message :: !Text
  , _sound :: !Text
  , _pinNumber :: !Int
  , _url :: !BaseUrl
  }

parseServiceUrl :: String -> ReadM BaseUrl
parseServiceUrl s =
  case runCatch $ parseBaseUrl s of
    Left _ -> readerError $ "Invalid service URL: " ++ s
    Right url -> return url

options :: Parser Options
options =
  Options <$>
  switch (long "quiet" <>
              short 'q' <>
              showDefault <>
              help "Only show errors") <*>
  option auto (long "interpreter" <>
               short 'i' <>
               metavar "SysfsIO" <>
               value SysfsIO <>
               showDefault <>
               help "Choose the GPIO interpreter to use") <*>
  option auto (long "edge" <>
               short 'e' <>
               metavar "RisingEdge|FallingEdge" <>
               value RisingEdge <>
               showDefault <>
               help "Trigger on rising/falling edge") <*>
  option auto (long "active-level" <>
               short 'l' <>
               metavar "ActiveLow|ActiveHigh" <>
               value ActiveHigh <>
               showDefault <>
               help "Pin active level") <*>
  option auto (long "debounce" <>
               short 'D' <>
               metavar "INT" <>
               value 5 <>
               showDefault <>
               help "Debounce duration in seconds")  <*>
  option text (long "headline" <>
               short 'H' <>
               metavar "TEXT" <>
               value (defaultNotification ^. headline) <>
               help "Override the default notification headline") <*>
  option text (long "message" <>
               short 'M' <>
               metavar "TEXT" <>
               value (defaultNotification ^. message) <>
               help "Override the default notification message") <*>
  option text (long "sound" <>
               short 'S' <>
               metavar "TEXT" <>
               value (defaultNotification ^. sound) <>
               help "Override the default notification sound") <*>
  argument auto (metavar "N" <>
                 help "GPIO pin number")  <*>
  argument (str >>= parseServiceUrl)
           (metavar "URL" <>
            help "PinPon server base URL")

-- Note: debounce delay here is in /microseconds/.
debounce :: (MonadIO m) => Int -> m a -> m a
debounce delay action =
  do startAction <- liftIO getCurrentTime
     result <- action
     endAction <- liftIO getCurrentTime
     let timeLeft = max 0 $ delay - toUsec (endAction `diffUTCTime` startAction)
       in liftIO $ threadDelay timeLeft
     return result
  where
    toUsec :: NominalDiffTime -> Int
    toUsec d = truncate $ d * 1000000

-- Not really that pretty.
prettyServantError :: ServantError -> Text
prettyServantError (FailureResponse _ status _ _) =
  T.unwords
    [pack (show $ statusCode status), pack (C8.unpack $ statusMessage status)]
prettyServantError DecodeFailure{} =
  "decode failure"
prettyServantError UnsupportedContentType{} =
  "unsupported content type"
prettyServantError InvalidContentTypeHeader{} =
  "invalid content type header"
prettyServantError ConnectionError{} =
  "connection refused"

run :: Options -> IO ()
run (Options quiet SysfsIO edge activeLevel debounceDelay hl msg s pin serviceUrl) =
  let notification = Notification hl msg s
  in do manager <- newManager tlsManagerSettings
        let clientEnv = ClientEnv manager serviceUrl
        runSysfsGpioIO $
          withInterruptPin (Pin pin) InputDefault edge (Just activeLevel) $ \h ->
          forever $ debounce (debounceDelay * 1000000) $ do
            void $ pollInterruptPin h
            output "Ring! Ring!"
            result <- sendNotification notification clientEnv
            case result of
              Right _ -> output "Notification sent"
              Left e -> outputErr $ T.unwords ["PinPon service error:", prettyServantError e]
  where
    sendNotification :: Notification -> ClientEnv -> SysfsGpioIO (Either ServantError Notification)
    sendNotification n env = liftIO $ runClientM (notify n) env

    output :: Text -> SysfsGpioIO ()
    output t = unless quiet $ liftIO (T.putStrLn t)

    outputErr :: Text -> SysfsGpioIO ()
    outputErr = liftIO . T.hPutStrLn stderr

main :: IO ()
main = execParser opts >>= run
  where
    opts =
      info (helper <*> options)
           (fullDesc <>
            progDesc "pinpon-gpio" <>
            header "A GPIO-driven PinPon doorbell client.")