packages feed

mercury-api-0.1.0.0: tests/replay.hs

{-# LANGUAGE OverloadedStrings #-}

import Control.Exception ( throw, try )
import Control.Monad ( when, void, forM_ )
import qualified Data.ByteString as B ( pack )
import Data.List ( maximumBy, delete )
import Data.Monoid ( (<>) )
import Data.Ord ( comparing )
import qualified Data.Text as T ( empty, pack )
import Options.Applicative
    ( Alternative(many),
      Applicative((<*>)),
      Parser,
      helper,
      execParser,
      value,
      switch,
      strOption,
      str,
      short,
      metavar,
      long,
      info,
      help,
      header,
      fullDesc,
      argument,
      (<$>) )
import System.Directory ( makeAbsolute )
import System.IO
    ( Handle,
      IOMode(ReadMode, WriteMode),
      withFile,
      hPutStrLn,
      hGetLine )
import System.Info ( os )

import qualified System.Hardware.MercuryApi as TMR
import qualified System.Hardware.MercuryApi.Params as TMR
import qualified System.Hardware.MercuryApi.Testing as TMR

data TestDirection = Record | Playback

data TestState =
  TestState
  { tsDirection :: TestDirection
  , tsHandle :: Handle
  }

type TestFunc = TMR.Reader -> TestState -> IO ()

suppressUri :: Either TMR.MercuryException a -> Either TMR.MercuryException a
suppressUri (Left exc) = Left exc { TMR.meUri = T.empty }
suppressUri x = x

check :: (Read a, Show a, Eq a) => TestState -> IO a -> IO a
check ts f = do
  eth' <- try f
  let eth = suppressUri eth'
  case tsDirection ts of
    Record -> hPutStrLn (tsHandle ts) (show eth)
    Playback -> do
      ln <- hGetLine (tsHandle ts)
      let expected = read ln :: (Read a => Either TMR.MercuryException a)
      when (expected /= eth) $ do
        putStrLn "expected:"
        putStrLn ln
        putStrLn "but got:"
        print eth
        fail "test failed"
  return $ case eth of
             Left exc -> throw exc -- only thrown if caller looks at result
             Right x -> x

runTest :: String -> TestDirection -> String -> TestFunc -> IO ()
runTest uri dir name func = do
  putStrLn $ "running test: " ++ name
  let fname = "tests/" ++ name
      transportFile = fname ++ ".transport"
      resultFile = fname ++ ".result"
  case dir of
    Record -> do
      withFile transportFile WriteMode $ \hTransport -> do
        withFile resultFile WriteMode $ \hResult -> do
          TMR.withReader (T.pack uri) $ \rdr -> do
            listener <- TMR.opcodeListener hTransport
            TMR.addTransportListener rdr listener
            TMR.paramSetTransportTimeout rdr 10000
            TMR.connect rdr
            func rdr (TestState dir hResult)
    Playback -> do
      absFile <- makeAbsolute transportFile
      withFile resultFile ReadMode $ \hResult -> do
        TMR.withReader (T.pack $ "test://" ++ absFile) $ \rdr -> do
          TMR.paramSetTransportTimeout rdr 10000
          TMR.connect rdr
          func rdr (TestState dir hResult)

setRegionAndPower :: TMR.Reader -> IO ()
setRegionAndPower rdr = do
  -- pwr <- TMR.paramGetRadioPowerMax rdr
  TMR.paramSetBasics rdr TMR.REGION_NA2 2200 TMR.sparkFunAntennas
  TMR.paramSetTagReadDataRecordHighestRssi rdr True

readUser =
  TMR.TagOp_GEN2_ReadData
  { TMR.opBank = TMR.GEN2_BANK_USER
  , TMR.opExtraBanks = []
  , TMR.opWordAddress = 0
  , TMR.opLen = 32
  }

emptyUserDataFilter :: TMR.TagFilter
emptyUserDataFilter = TMR.mkFilterGen2 TMR.GEN2_BANK_USER 0 $ B.pack [0, 0]

testParams :: TestFunc
testParams rdr ts = do
  params <- check ts $ TMR.paramList rdr
  let params' = TMR.PARAM_URI `delete` params -- because URI will be different
  forM_ params' $ \param -> do
    check ts $ TMR.paramGetString rdr param

testRead :: TestFunc
testRead rdr ts = do
  setRegionAndPower rdr

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  forM_ tags $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

testReadUser :: TestFunc
testReadUser rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanTagop rdr (Just readUser)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  forM_ tags $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

testWrite :: TestFunc
testWrite rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just emptyUserDataFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      words = TMR.packBytesIntoWords "I am Groot"
      opWrite = TMR.TagOp_GEN2_WriteData
                { TMR.opBank = TMR.GEN2_BANK_USER
                , TMR.opWordAddress = 0
                , TMR.opData = words
                }
  check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  TMR.paramSetReadPlanFilter rdr Nothing
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  check ts $ return $ length tags2
  forM_ tags2 $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

testWriteEpc :: TestFunc
testWriteEpc rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just emptyUserDataFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let tag = TMR.trTag trd
      epcFilt = TMR.TagFilterEPC tag
      Just newEpc = TMR.hexToBytes "0123456789abcdef"
      newTag = tag { TMR.tdEpc = newEpc }
      opWrite = TMR.TagOp_GEN2_WriteTag newTag

  check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  TMR.paramSetReadPlanFilter rdr Nothing
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  check ts $ return $ length tags2
  forM_ tags2 $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

  let epcFilt2 = TMR.TagFilterEPC newTag
      opWrite2 = TMR.TagOp_GEN2_WriteTag tag

  void $ check ts $ TMR.executeTagOp rdr opWrite2 (Just epcFilt2)

testLock :: TestFunc
testLock rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just emptyUserDataFilter)

  -- find a tag
  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  -- write access password
  let password = 12345
      epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      opWrite = TMR.TagOp_GEN2_WriteData
                { TMR.opBank = TMR.GEN2_BANK_RESERVED
                , TMR.opWordAddress = TMR.accessPasswordAddress
                , TMR.opData = TMR.passwordToWords password
                }
  check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  -- lock user bank
  let opLock = TMR.TagOp_GEN2_Lock
               { TMR.opMask   = [TMR.GEN2_LOCK_BITS_USER]
               , TMR.opAction = [TMR.GEN2_LOCK_BITS_USER]
               , TMR.opAccessPassword = password
               }
  check ts $ TMR.executeTagOp rdr opLock (Just epcFilt)

  -- attempt to write data; should fail
  let opWrite2 = TMR.TagOp_GEN2_WriteData
                 { TMR.opBank = TMR.GEN2_BANK_USER
                 , TMR.opWordAddress = 0
                 , TMR.opData = TMR.packBytesIntoWords "This should fail"
                 }
  check ts $ TMR.executeTagOp rdr opWrite2 (Just epcFilt)

  -- unlock user bank
  let opUnlock = TMR.TagOp_GEN2_Lock
                 { TMR.opMask   = [TMR.GEN2_LOCK_BITS_USER]
                 , TMR.opAction = []
                 , TMR.opAccessPassword = password
                 }
  check ts $ TMR.executeTagOp rdr opUnlock (Just epcFilt)

  -- attempt to write data; should succeed
  let opWrite3 = TMR.TagOp_GEN2_WriteData
                 { TMR.opBank = TMR.GEN2_BANK_USER
                 , TMR.opWordAddress = 0
                 , TMR.opData = TMR.packBytesIntoWords "This should succeed"
                 }
  void $ check ts $ TMR.executeTagOp rdr opWrite3 (Just epcFilt)

sequentialBytes = B.pack [0..7]

testBlockWrite :: TestFunc
testBlockWrite rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just emptyUserDataFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      words = TMR.packBytesIntoWords sequentialBytes
      opWrite = TMR.TagOp_GEN2_BlockWrite
                { TMR.opBank = TMR.GEN2_BANK_USER
                , TMR.opWordPtr = 0
                , TMR.opData = words
                }
  check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  TMR.paramSetReadPlanFilter rdr Nothing
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  check ts $ return $ length tags2
  forM_ tags2 $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

sequentialUserDataFilter :: TMR.TagFilter
sequentialUserDataFilter = TMR.mkFilterGen2 TMR.GEN2_BANK_USER 0 sequentialBytes

testBlockErase :: TestFunc
testBlockErase rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just sequentialUserDataFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      opErase = TMR.TagOp_GEN2_BlockErase
                { TMR.opBank = TMR.GEN2_BANK_USER
                , TMR.opWordPtr = 0
                , TMR.opWordCount = 4
                }
  check ts $ TMR.executeTagOp rdr opErase (Just epcFilt)

  TMR.paramSetReadPlanFilter rdr Nothing
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  check ts $ return $ length tags2
  forM_ tags2 $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

permalockMeFilter :: TMR.TagFilter
permalockMeFilter = TMR.mkFilterGen2 TMR.GEN2_BANK_USER 0 "permalock me"

testBlockPermalockRead :: TestFunc
testBlockPermalockRead rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just permalockMeFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      opPermalock = TMR.TagOp_GEN2_BlockPermaLock
                    { TMR.opBank = TMR.GEN2_BANK_USER
                    , TMR.opBlockPtr = 0
                    , TMR.opReadWrite = TMR.Read 1
                    }
  void $ check ts $ TMR.executeTagOp rdr opPermalock (Just epcFilt)

testBlockPermalockWrite :: TestFunc
testBlockPermalockWrite rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just permalockMeFilter)

  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  let epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      opPermalock = TMR.TagOp_GEN2_BlockPermaLock
                    { TMR.opBank = TMR.GEN2_BANK_USER
                    , TMR.opBlockPtr = 0
                    , TMR.opReadWrite = TMR.Write [0xaa00]
                    }
  check ts $ TMR.executeTagOp rdr opPermalock (Just epcFilt)

  forM_ [0..31] $ \word -> do
    let opWrite = TMR.TagOp_GEN2_WriteData
                  { TMR.opBank = TMR.GEN2_BANK_USER
                  , TMR.opWordAddress = word
                  , TMR.opData = [fromIntegral word]
                  }
    check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  TMR.paramSetReadPlanFilter rdr Nothing
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  check ts $ return $ length tags2
  forM_ tags2 $ \tag -> do
    check ts $ return tag { TMR.trTimestamp = 0 }

  let opPermalock2 = TMR.TagOp_GEN2_BlockPermaLock
                     { TMR.opBank = TMR.GEN2_BANK_USER
                     , TMR.opBlockPtr = 0
                     , TMR.opReadWrite = TMR.Read 1
                     }
  void $ check ts $ TMR.executeTagOp rdr opPermalock2 (Just epcFilt)

killMeFilter :: TMR.TagFilter
killMeFilter =
  TMR.mkFilterGen2 TMR.GEN2_BANK_USER 0 $ "permaloc" <> B.pack [0,4,0,5,0,6,0,7]

testKill :: TestFunc
testKill rdr ts = do
  setRegionAndPower rdr
  TMR.paramSetReadPlanFilter rdr (Just killMeFilter)

  -- find a tag
  tags <- TMR.read rdr 1000
  check ts $ return $ length tags
  let trd = maximumBy (comparing TMR.trRssi) tags
  check ts $ return trd { TMR.trTimestamp = 0 }

  -- write access password
  let password = 0xbeadead1
      epcFilt = TMR.TagFilterEPC (TMR.trTag trd)
      opWrite = TMR.TagOp_GEN2_WriteData
                { TMR.opBank = TMR.GEN2_BANK_RESERVED
                , TMR.opWordAddress = TMR.killPasswordAddress
                , TMR.opData = TMR.passwordToWords password
                }
  check ts $ TMR.executeTagOp rdr opWrite (Just epcFilt)

  -- kill
  let opKill = TMR.TagOp_GEN2_Kill password
  check ts $ TMR.executeTagOp rdr opKill (Just epcFilt)

  -- still alive?
  TMR.paramSetReadPlanFilter rdr (Just epcFilt)
  TMR.paramSetReadPlanTagop rdr (Just readUser)
  tags2 <- TMR.read rdr 1000
  void $ check ts $ return $ length tags2

mkPin :: TMR.PinNumber -> TMR.PinNumber -> TMR.GpioPin
mkPin highPin pin =
  TMR.GpioPin
  { TMR.gpId = pin
  , TMR.gpHigh = highPin == pin
  , TMR.gpOutput = True
  }

testGpo :: TestFunc
testGpo rdr ts = do
  let pins = [1..4]

  TMR.paramSetGpioOutputList rdr pins
  forM_ pins $ \pin -> do
    TMR.gpoSet rdr $ map (mkPin pin) pins

testGpi :: TestFunc
testGpi rdr ts = do
  let pins = [1..4]

  TMR.paramSetGpioInputList rdr pins
  void $ check ts $ TMR.gpiGet rdr

tests :: [(String, TestFunc)]
tests =
  [ ("params", testParams)
  , ("read", testRead)
  , ("readUser", testReadUser)
  , ("write", testWrite)
  , ("writeEpc", testWriteEpc)
  , ("lock", testLock)
  , ("blockWrite", testBlockWrite)
  , ("blockErase", testBlockErase)
  , ("blockPermalockRead", testBlockPermalockRead)
  , ("blockPermalockWrite", testBlockPermalockWrite)
  , ("kill", testKill)
  , ("gpo", testGpo)
  , ("gpi", testGpi)
  ]

allTests = map fst tests

runTests :: String -> TestDirection -> [String] -> IO ()
runTests uri dir ts = do
  forM_ ts $ \t -> do
    let mf = t `lookup` tests
    case mf of
      Nothing -> fail $ "no test named " ++ t
      Just f -> runTest uri dir t f

defUri :: String
defUri = case os of
           "darwin" -> "tmr:///dev/cu.SLAB_USBtoUART"
           "mingw32" -> "tmr:///COM4"
           _ -> "tmr:///dev/ttyUSB0"

data Opts = Opts
  { oUri :: String
  , oRecord :: Bool
  , oTests :: [String]
  }

optUri :: Parser String
optUri = strOption (long "uri" <>
                    short 'u' <>
                    metavar "URI" <>
                    help ("Reader to connect to (default " ++ defUri ++ ")") <>
                    value defUri)

optRecord :: Parser Bool
optRecord = switch (long "record" <>
                    short 'R' <>
                    help "record a new test from a physical reader")

opts :: Parser Opts
opts = Opts
  <$> optUri
  <*> optRecord
  <*> many (argument str (metavar "TESTS..."))

opts' = info (helper <*> opts)
  ( fullDesc <>
    header "replay - automated tests that use a simulated reader" )

main = do
  TMR.registerTransportInit
  o <- execParser opts'

  let dir = if oRecord o then Record else Playback
  let ts = case oTests o of
             [] -> allTests
             xs -> xs

  runTests (oUri o) dir ts