packages feed

obd-0.2.0.0: src/System/Hardware/ELM327/Simulator.hs

-- | Module that simulates an ELM327 device.
module System.Hardware.ELM327.Simulator (
  -- * Initializing the simulator
  Simulator(..)
, defaultSimulator

  -- * Interacting with the simulator
, connect
, handle

  -- * Attributes of the simulator
, ConnectedProtocol(..)
, Echo(..)
, obdBus
, chosenProtocol
, connectedProtocol
, echo
, pin2Voltage
, versionID
) where

import Control.Concurrent (forkIO)
import Control.Concurrent.STM (TMVar, atomically, newTMVarIO, takeTMVar, putTMVar)
import Control.Concurrent.STM.TMChan (TMChan, newTMChanIO, closeTMChan, readTMChan, writeTMChan)
import Control.Lens (Lens', lens, (^.), (^?), (.~), re)
import Control.Monad (void)
import Control.Monad.State (StateT, runState, runStateT, get, put)
import Control.Monad.IO.Class (liftIO)
import Data.ByteString.Char8 (ByteString)
import Data.Char (isHexDigit, toUpper)
import Text.Printf (printf)
import qualified Data.ByteString.Char8 as Char8

import Numeric.Units.Dimensional.Prelude (ElectricPotential, volt, (*~), (/~))

import System.IO.Streams (makeInputStream, makeOutputStream)

import System.Hardware.ELM327.Commands (AT(..), Command(..), command,
                                        Protocol(..), protocol,
                                        obdMode, obdPID)
import System.Hardware.ELM327.Connection (Con(..))
import System.Hardware.ELM327.Simulator.OBDBus (OBDBus)
import System.Hardware.ELM327.Utils.Hex (bytesToHex)
import qualified System.Hardware.ELM327.Simulator.OBDBus as OBDBus

-- | An actual simulator, with underlying bus, that holds all state.
data Simulator bus = Simulator { _obdBus :: bus
                               , _chosenProtocol :: Protocol
                               , _connectedProtocol :: ConnectedProtocol
                               , _echo :: Echo
                               , _pin2Voltage :: ElectricPotential Double
                               , _versionID :: String }
                               deriving (Show)

-- | The default simulator, initialized with a given protocol
defaultSimulator :: OBDBus bus => bus -> Simulator bus
defaultSimulator b = Simulator { _obdBus = b
                               , _chosenProtocol = AutomaticProtocol
                               , _connectedProtocol = NotConnected
                               , _echo = EchoOff
                               , _pin2Voltage = 12.3 *~ volt
                               , _versionID = "ELM327 v2.1" }

-- | Private data declaration that represents a connection to the simulator.
data SimCon bus = SimCon { _conInputBuffer :: TMChan ByteString
                         , _conOutputBuffer :: TMChan ByteString
                         , _unhandledInput :: TMVar ByteString }

-- | Initialize a 'SimulatorCon'
simCon :: IO (SimCon bus)
simCon = do
    ib <- newTMChanIO
    ob <- newTMChanIO
    ui <- newTMVarIO ""
    return SimCon { _conInputBuffer = ib
             , _conOutputBuffer = ob
             , _unhandledInput = ui }

-- | Connect to the simulator.
connect :: OBDBus bus => Simulator bus -> IO Con
connect s = do
    c <- simCon
    is <- makeInputStream (produce c)
    os <- makeOutputStream (consume c)
    _ <- forkIO . void $ runStateT (runSimulator c) s
    return $ Con is os
  where
    produce = atomically . readTMChan . _conOutputBuffer
    consume c Nothing = atomically $ closeTMChan (_conInputBuffer c)
    consume c (Just x) = atomically $ writeTMChan (_conInputBuffer c) x

    runSimulator :: OBDBus bus => SimCon bus -> StateT (Simulator bus) IO ()
    runSimulator c = do
        next <- liftIO $ recv c
        case next of
            Nothing -> liftIO . atomically $ closeTMChan (_conOutputBuffer c)
            Just cmd -> do response <- handle (Char8.unpack cmd)
                           liftIO . atomically $ writeTMChan (_conOutputBuffer c) (Char8.pack response)
                           runSimulator c

    recv c = atomically $ recv' c =<< takeTMVar (_unhandledInput c)
    recv' c bs = do
        let (pref, suf) = Char8.break (== '\r') bs
        if Char8.null suf then do next <- readTMChan (_conInputBuffer c)
                                  case next of Nothing -> return Nothing
                                               Just x -> recv' c (Char8.append bs x)
                          else do putTMVar (_unhandledInput c) (Char8.tail suf)
                                  return $ Just pref

-- | Private data declaration that represents a parsed command (used in 'handle')
data ParsedCommand = InvalidCommand
                   | KnownCommand Command
                   | UnknownOBD String

-- | Parse a command from a serial line
parsedCommand :: String -> ParsedCommand
parsedCommand = parsedCommand' . map toUpper . filter (/= ' ')
  where
    parsedCommand' c@('A':'T':_) = maybe InvalidCommand KnownCommand (c ^? command)
    parsedCommand' c | all isHexDigit c = maybe (UnknownOBD c) KnownCommand (c ^? command)
                     | otherwise        = InvalidCommand

-- | Make the simulator handle a command.
handle :: (Monad m, OBDBus bus) => String -> StateT (Simulator bus) m String
handle cmd = handle' $ parsedCommand cmd
  where
    handle' :: (Monad m, OBDBus bus) => ParsedCommand -> StateT (Simulator bus) m String
    handle' InvalidCommand = return "?"
    handle' (UnknownOBD _) = reply . either id (++ "NO DATA") <$> connectToBus
    handle' (KnownCommand x) = handle'' x
    handle'' (OBD x) = do
        connectStatus <- connectToBus
        case connectStatus of
            Left errMsg -> return $ reply errMsg
            Right conMsg -> do
                bus <- (^. obdBus) <$> get
                let (resp, bus') = runState (OBDBus.request x) bus
                (obdBus .~ bus') <$> get >>= put
                case resp of
                    Nothing -> return . reply $ conMsg ++ "NO DATA"
                    Just v -> return . reply . (conMsg ++) . bytesToHex " " $ [obdMode x + 0x40, obdPID x] ++ v
    handle'' (AT ATDescribeProtocolNumber) = reply <$> describeProtocolNumber
    handle'' (AT ATEchoOff) = reply <$> echoOff
    handle'' (AT ATReadVoltage) = reply <$> readVoltage
    handle'' (AT ATResetAll) = reply <$> resetAll
    handle'' (AT (ATSelectProtocol p)) = reply <$> selectProtocol p
    handle'' (AT ATVersionID) = reply . (^. versionID) <$> get

    reply = (++ "\r>")

-- | The protocol the ELM327 simulator is setup to use.
data ConnectedProtocol = NotConnected
                       | ManuallyChosen Protocol
                       | AutomaticallyChosen Protocol
                       deriving (Eq, Show)

-- | The echo settings of the ELM327.
data Echo = EchoOn | EchoOff deriving (Eq, Show)

-- | The actual underlying simulated bus of the simulator.
obdBus :: OBDBus bus => Lens' (Simulator bus) bus
obdBus = lens _obdBus $ \s x -> s { _obdBus = x }

-- | The protocol the simulator should try to connect with.
chosenProtocol :: Lens' (Simulator bus) Protocol
chosenProtocol = lens _chosenProtocol $ \s x -> s { _chosenProtocol = x }

-- | The protocol the ELM327 simulator is connected with.
connectedProtocol :: Lens' (Simulator bus) ConnectedProtocol
connectedProtocol = lens _connectedProtocol $ \s x -> s { _connectedProtocol = x }

-- | The echo settings of the ELM327.
echo :: Lens' (Simulator bus) Echo
echo = lens _echo $ \s x -> s { _echo = x }

-- | The voltage on pin 2 (most likely the battery voltage)
pin2Voltage :: Lens' (Simulator bus) (ElectricPotential Double)
pin2Voltage = lens _pin2Voltage $ \s x -> s { _pin2Voltage = x }

-- | The version ID of the ELM327.
versionID :: Lens' (Simulator bus) String
versionID = lens _versionID $ \s x -> s { _versionID = x }

-- | If the command is just a setting change, the ELM327 will reply with OK.
replyOK :: Monad m => StateT (Simulator bus) m String
replyOK = return "OK"

-- | Connect to the underlying bus
--
-- If the connection fails, the result will be 'Left errorMessage', otherwise
-- the result will be 'Right statusMessage'.
connectToBus :: (Monad m, OBDBus bus) => StateT (Simulator bus) m (Either String String)
connectToBus = do
    cp     <- (^. connectedProtocol) <$> get
    bus    <- OBDBus.protocol . (^. obdBus) <$> get
    chosen <- (^. chosenProtocol) <$> get
    case (cp, chosen) of
        (NotConnected, AutomaticProtocol) -> do
            (connectedProtocol .~ AutomaticallyChosen bus) <$> get >>= put
            return $ Right "SEARCHING..."
        (NotConnected, _) ->
            if bus /= chosen
                then return $ Left "UNABLE TO CONNECT"
                else do (connectedProtocol .~ ManuallyChosen bus) <$> get >>= put
                        return $ Right ""
        (_, _) -> return $ Right ""

-- | Describe the protocol number
describeProtocolNumber :: Monad m => StateT (Simulator bus) m String
describeProtocolNumber = do
    cp <- (^. connectedProtocol) <$> get
    case cp of NotConnected -> return $ "A" ++ AutomaticProtocol ^. re protocol
               ManuallyChosen p -> return $ p ^. re protocol
               AutomaticallyChosen p -> return $ "A" ++ p ^. re protocol

-- | Turn echo off
echoOff :: Monad m => StateT (Simulator bus) m String
echoOff = (echo .~ EchoOff) <$> get >>= put >> replyOK

-- | Read the battery voltage
readVoltage :: Monad m => StateT (Simulator bus) m String
readVoltage = printf "%.1f" . (/~ volt) . (^. pin2Voltage) <$> get

-- | Fully reset the device
resetAll :: (Monad m, OBDBus bus) => StateT (Simulator bus) m String
resetAll = defaultSimulator . (^. obdBus) <$> get >>= put >> return ""

-- | Select a protocol
selectProtocol :: Monad m => Protocol -> StateT (Simulator bus) m String
selectProtocol p = (chosenProtocol .~ p) <$> get >>= put >> replyOK