packages feed

obd-0.2.0.0: app/Terminal.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Main where

import Control.Exception (bracket)
import Control.Monad (join)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Class (lift)
import Data.List (isSuffixOf)
import qualified Data.ByteString.Char8 as Char8

import Options.Applicative (Parser, execParser,
                            flag, strOption,
                            help, long, value,
                            info, progDesc,
                            (<>))
import Options.Applicative.Extra (helper)

import System.Console.Haskeline (InputT, MonadException, RunIO(..),
                                 controlIO, runInputT,
                                 defaultSettings, getInputLine)

import System.Hardware.ELM327.Connection (Con, ConT, ConError, withCon, recv, sendString)
import System.Hardware.ELM327.Simulator (defaultSimulator)
import System.Hardware.ELM327.Simulator.OBDBus.VWPolo2007 (stoppedCarBus)
import qualified System.Hardware.ELM327.Connection as Connection
import qualified System.Hardware.ELM327 as ELM327
import qualified System.Hardware.ELM327.Simulator as Simulator


newtype TermT a = TermT { runTermT :: ConT IO a }
                        deriving (Functor, Applicative, Monad, MonadIO)

instance MonadException TermT where
    controlIO f = join . liftIO $ f (RunIO return)

data ConnectionType = ConnectionTypeActualDevice FilePath
                    | ConnectionTypeSimulator

connectionType :: Parser ConnectionType
connectionType = flag ConnectionTypeActualDevice (const ConnectionTypeSimulator) m <*> serialPort
    where m = long "simulator" <> help "Connect to the simulator instead of a serial port"

serialPort :: Parser FilePath
serialPort = strOption m
    where m = long "port" <> value "/dev/ttyUSB0" <> help "The serial port to connect to"

main :: IO ()
main = execParser i >>= runTerminal
    where i = info p $ progDesc "Connect a terminal to an ELM327 device or simulator."
          p = helper <*> connectionType

connect :: ConnectionType -> IO Con
connect ConnectionTypeSimulator          = Simulator.connect (defaultSimulator stoppedCarBus)
connect (ConnectionTypeActualDevice dev) = either (\e -> fail $ "could not connect: " ++ show e) return =<< ELM327.connect dev

runTerminal :: ConnectionType -> IO ()
runTerminal ct = do
    r <- bracket (connect ct)
                 (\c -> putStrLn "Closing connection..." >> Connection.close' c)
                 (liftIO . runTerminalWithConnection)
    either (\e -> fail $ "error: " ++ show e) return r

runTerminalWithConnection :: Con -> IO (Either ConError ())
runTerminalWithConnection con = withCon con (runTermT readEvalPrint)

readEvalPrint :: TermT ()
readEvalPrint = runInputT defaultSettings loop
  where
    loop :: InputT TermT ()
    loop = do
        maybeLine <- getInputLine "% "
        case maybeLine of Nothing -> return ()
                          Just ":exit" -> return ()
                          Just x -> handleLine x >> loop

handleLine :: String -> InputT TermT ()
handleLine cmd = do
    lift $ TermT . sendString $ cmd ++ "\r"
    printAll

printAll :: InputT TermT ()
printAll = do
    maybeBS <- lift $ TermT recv
    case maybeBS of Nothing -> return ()
                    Just b -> liftIO . putStrLn $ convertResponse b
  where
    convertResponse = stripPrompt . filter (/= '\0') . Char8.unpack
    stripPrompt :: String -> String
    stripPrompt x = if "\n>" `isSuffixOf` x then take (length x - 1) x else x