packages feed

vrpn-0.2.0.0: src/Network/VRPN.hs

{-|
Module      :  Network.VRPN
Copyright   :  (c) 2015 Brian W Bush
License     :  MIT
Maintainer  :  Brian W Bush <consult@brianwbush.info>
Stability   :  Stable
Portability :  Portable

Bindings to VRPN, \<<https://github.com/vrpn/vrpn/wiki>\>, and is loosely modeled on the code in \<<https://github.com/vrpn/vrpn/blob/master/client_src/vrpn_print_devices.C>\>.  This has been tested using VRPN 07.30 on Linux.  It requires the VRPN C++ header files.

Here is a simple example that illustrates the use of this module:

@
data ButtonType = LeftButton | RightButton
  deriving (Enum, Eq, Show)

main :: IO ()
main =
  do
    putStrLn "Press the left button to exit."
    done <- newEmptyMVar
    let
      -- A remote button that signals completion when the left button is released.
      button :: Device Int ButtonType Double
      button =
        Button "spacenav0@localhost"
          $ Just
          $ \time button state ->
            do
              print (time, button, state)
              if button == LeftButton && not state
                then void $ tryPutMVar done ()
                else return ()
      -- An analog device.
      analog :: Device Int Int Int Double
      analog = Analog "spacenav0@localhost"
        $ Just
        $ curry print
    -- Open the remote devices.
    devices <- sequence [openDevice button, openDevice analog]
    -- Loop until a signal to complete is received.
    mainLoops (not <$> isEmptyMVar done) 10 devices
    -- Close the remote devices.
    mapM_ closeDevice devices
@
-}


{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE RecordWildCards          #-}


module Network.VRPN (
-- * Devices
  Device(..)
, RemoteDevice
-- * Callbacks
, PositionCallback
, VelocityCallback
, AccelerationCallback
, ButtonCallback
, AnalogCallback
, DialCallback
, ExitCallback
-- * Operations on devices
, openDevice
, closeDevice
, mainLoop
, mainLoops
-- * Time
, TimeVal(..)
, sleep
) where


import Control.Monad (unless)
import Foreign.C.String (CString, withCString) 
import Foreign.C.Types (CDouble(..), CInt(..), CLong(..))
import Foreign.Concurrent (newForeignPtr)
import Foreign.Marshal.Array (peekArray)
import Foreign.ForeignPtr (ForeignPtr, finalizeForeignPtr, withForeignPtr)
import Foreign.Ptr (FunPtr, Ptr, freeHaskellFunPtr, nullFunPtr)


-- | A VRPN device.
data Device s b d a =
    -- | A tracker.
    Tracker
    {
      device               :: String                           -- ^ The device name.
    , positionCallback     :: Maybe (PositionCallback s a)     -- ^ The position callback.
    , velocityCallback     :: Maybe (VelocityCallback s a)     -- ^ The velocity callback.
    , accelerationCallback :: Maybe (AccelerationCallback s a) -- ^ The acceleration callback.
    }
    -- | A button.
  | Button
    {
      device               :: String                           -- ^ The device name.
    , buttonCallback       :: Maybe (ButtonCallback b)         -- ^ The button callback.
    }
    -- | An analog device.
  | Analog
    {
      device               :: String                           -- ^ The device name.
    , analogCallback       :: Maybe (AnalogCallback a)         -- ^ The analog callback.
    }
    -- | A dial.
  | Dial
    {
      device               :: String                           -- ^ The device name.
    , dialCallback         :: Maybe (DialCallback d a)         -- ^ The dial callback.
    }


-- | Timestamps in seconds and fractions of a section.
data TimeVal =
  TimeVal
  {
    timeSeconds      :: Int -- ^ The seconds.
  , timeMicroSeconds :: Int -- ^ The microseconds.
  }
    deriving (Eq, Ord, Read, Show)


-- | Callback for position information.
type PositionCallback s a =  TimeVal      -- ^ The timestamp.
                          -> s            -- ^ Which sensor is reporting.
                          -> (a, a, a)    -- ^ The position vector.
                          -> (a, a, a, a) -- ^ The orientation quaternion.
                          -> IO ()        -- ^ The action performed by the callback.


-- | Callback for position information.
type PositionCallback' =  CLong   -- ^ Seconds of the timestamp.
                       -> CLong   -- ^ Microseconds of the timestamp.
                       -> CInt    -- ^ Which sensor is reporting.
                       -> CDouble -- ^ 1st component of the position vector.
                       -> CDouble -- ^ 2nd component of the position vector.
                       -> CDouble -- ^ 3rd component of the position vector.
                       -> CDouble -- ^ 1st component of the orientation quaternion.
                       -> CDouble -- ^ 2nd component of the orientation quaternion.
                       -> CDouble -- ^ 3rd component of the orientation quaternion.
                       -> CDouble -- ^ 4th component of the orientation quaternion.
                       -> IO ()   -- ^ The action performed by the callback.

-- | Wrap a position callback.
foreign import ccall "wrapper"
  wrapPositionCallback :: PositionCallback' -> IO (FunPtr PositionCallback')


-- | Make a position callback suitable for FFI.
makePositionCallback :: (Enum s, RealFloat a)
                     => PositionCallback s a -- ^ The callback.
                     -> PositionCallback'    -- ^ An equivalent FFI callback.
makePositionCallback callback seconds microseconds sensor px py pz ox oy oz ow =
  callback
    (TimeVal (fromEnum seconds) (fromEnum microseconds))
    (toEnum $ fromEnum sensor)
    (realToFrac px, realToFrac py, realToFrac pz)
    (realToFrac ox, realToFrac oy, realToFrac oz, realToFrac ow)


-- | Callback for velocity information.
type VelocityCallback s a =  TimeVal      -- ^ The timestamp.
                          -> s            -- ^ Which sensor is reporting.
                          -> (a, a, a)    -- ^ The velocity vector.
                          -> (a, a, a, a) -- ^ The future orientation quaternion.
                          -> a            -- ^ Delta time for the future orientation quaternion, in seconds.
                          -> IO ()        -- ^ The action performed by the callback.


-- | Callback for velocity information.
type VelocityCallback' =  CLong   -- ^ Seconds of the timestamp.
                       -> CLong   -- ^ Microseconds of the timestamp.
                       -> CInt    -- ^ Which sensor is reporting.
                       -> CDouble -- ^ 1st component of the velocity vector.
                       -> CDouble -- ^ 2nd component of the velocity vector.
                       -> CDouble -- ^ 3rd component of the velocity vector.
                       -> CDouble -- ^ 1st component of the future orientation quaternion.
                       -> CDouble -- ^ 2nd component of the future orientation quaternion.
                       -> CDouble -- ^ 3rd component of the future orientation quaternion.
                       -> CDouble -- ^ 4th component of the future orientation quaternion.
                       -> CDouble -- ^ Delta time for the future orientation quaternion, in seconds.
                       -> IO ()   -- ^ The action performed by the callback.


-- | Wrap a velocity callback.
foreign import ccall "wrapper"
  wrapVelocityCallback :: VelocityCallback' -> IO (FunPtr VelocityCallback')


-- | Make a velocity callback suitable for FFI.
makeVelocityCallback :: (Enum s, RealFloat a)
                     => VelocityCallback s a -- ^ The callback.
                     -> VelocityCallback'    -- ^ An equivalent FFI callback.
makeVelocityCallback callback seconds microseconds sensor vx vy vz ox oy oz ow dt =
  callback
    (TimeVal (fromEnum seconds) (fromEnum microseconds))
    (toEnum $ fromEnum sensor)
    (realToFrac vx, realToFrac vy, realToFrac vz)
    (realToFrac ox, realToFrac oy, realToFrac oz, realToFrac ow)
    (realToFrac dt)


-- | Callback for acceleration information.
type AccelerationCallback s a =  TimeVal      -- ^ The timestamp.
                              -> s            -- ^ Which sensor is reporting.
                              -> (a, a, a)    -- ^ The acceleration vector.
                              -> (a, a, a, a) -- ^ The acceleration orientation quaternion.
                              -> a            -- ^ Delta time for the acceleration quaternion, in seconds.
                              -> IO ()        -- ^ The action performed by the callback.


-- | Callback for acceleration information.
type AccelerationCallback' =  CLong   -- ^ Seconds of the timestamp.
                           -> CLong   -- ^ Microseconds of the timestamp.
                           -> CInt    -- ^ Which sensor is reporting.
                           -> CDouble -- ^ 1st component of the acceleration vector.
                           -> CDouble -- ^ 2nd component of the acceleration vector.
                           -> CDouble -- ^ 3rd component of the acceleration vector.
                           -> CDouble -- ^ 1st component of the acceleration quaternion.
                           -> CDouble -- ^ 2nd component of the acceleration quaternion.
                           -> CDouble -- ^ 3rd component of the acceleration quaternion.
                           -> CDouble -- ^ 4th component of the acceleration quaternion.
                           -> CDouble -- ^ Delta time for the acceleration quaternion, in seconds.
                           -> IO ()   -- ^ The action performed by the callback.


-- | Wrap an acceleration callback.
foreign import ccall "wrapper"
  wrapAccelerationCallback :: AccelerationCallback' -> IO (FunPtr AccelerationCallback')


-- | Make an acceleration callback suitable for FFI.
makeAccelerationCallback :: (Enum s, RealFloat a)
                         => AccelerationCallback s a -- ^ The callback.
                         -> AccelerationCallback'    -- ^ An equivalent FFI callback.
makeAccelerationCallback callback seconds microseconds sensor ax ay az ox oy oz ow dt =
  callback
    (TimeVal (fromEnum seconds) (fromEnum microseconds))
    (toEnum $ fromEnum sensor)
    (realToFrac ax, realToFrac ay, realToFrac az)
    (realToFrac ox, realToFrac oy, realToFrac oz, realToFrac ow)
    (realToFrac dt)


-- | Callback for button information.
type ButtonCallback b =  TimeVal -- ^ The timestamp.
                      -> b       -- ^ Which button was pressed, counting from 0.
                      -> Bool    -- ^ Whether the button is pressed.
                      -> IO ()   -- ^ The action performed by the callback.


-- | Callback for button information.
type ButtonCallback' =  CLong   -- ^ Seconds of the timestamp.
                     -> CLong   -- ^ Microseconds of the timestamp.
                     -> CInt    -- ^ Which button was pressed, counting from 0.
                     -> CInt    -- ^ The button state (0 = off, 1 = on).
                     -> IO ()   -- ^ The action performed by the callback.


-- | Wrap a button callback.
foreign import ccall "wrapper"
  wrapButtonCallback :: ButtonCallback' -> IO (FunPtr ButtonCallback')


-- | Make a button callback suitable for FFI.
makeButtonCallback :: Enum b
                   => ButtonCallback b -- ^ The callback.
                   -> ButtonCallback'  -- ^ An equivalent FFI callback.
makeButtonCallback callback seconds microseconds button state =
  callback
    (TimeVal (fromEnum seconds) (fromEnum microseconds))
    (toEnum $ fromEnum button)
    (state /= 0)


-- | Callback for analog information.
type AnalogCallback a =  TimeVal -- ^ The timestamp.
                      -> [a]     -- ^ The analog values.
                      -> IO ()   -- ^ The action performed by the callback.


-- | Callback for analog information.
type AnalogCallback' =  CLong      -- ^ Seconds of the timestamp.
                     -> CLong      -- ^ Microseconds of the timestamp.
                     -> CInt        -- ^ The number of values.
                     -> Ptr CDouble -- ^ The analog values.
                     -> IO ()       -- ^ The action performed by the callback.


-- | Wrap an analog callback.
foreign import ccall "wrapper"
  wrapAnalogCallback :: AnalogCallback' -> IO (FunPtr AnalogCallback')


-- | Make an analog callback suitable for FFI.
makeAnalogCallback :: RealFloat a
                   => AnalogCallback a -- ^ The callback.
                   -> AnalogCallback'  -- ^ An equivalent FFI callback.
makeAnalogCallback callback seconds microseconds n ptr =
  do
    values <- peekArray (fromEnum n) ptr
    callback
      (TimeVal (fromEnum seconds) (fromEnum microseconds))
      (map realToFrac values)


-- | Callback for dial information.
type DialCallback d a =  TimeVal -- ^ The timestamp.
                      -> d       -- ^ Which dial changed.
                      -> a       -- ^ The fraction of a revolution it changed.
                      -> IO ()   -- ^ The action performed by the callback.


-- | Callback for dial information.
type DialCallback' =  CLong   -- ^ Seconds of the timestamp.
                   -> CLong   -- ^ Microseconds of the timestamp.
                   -> CInt    -- ^ Which dial changed.
                   -> CDouble -- ^ The fraction of a revolution it changed.
                   -> IO ()   -- ^ The action performed by the callback.


-- | Wrap a dial callback.
foreign import ccall "wrapper"
  wrapDialCallback :: DialCallback' -> IO (FunPtr DialCallback')


-- | Make an analog callback suitable for FFI.
makeDialCallback :: (Enum d, RealFloat a)
                 => DialCallback d a -- ^ The callback.
                 -> DialCallback'    -- ^ An equivalent FFI callback.
makeDialCallback callback seconds microseconds dial value =
  callback
    (TimeVal (fromEnum seconds) (fromEnum microseconds))
    (toEnum $ fromEnum dial)
    (realToFrac value)


-- | Callback for exiting the main loop.
type ExitCallback = IO Bool -- ^ An action indicate whether to exit the main loop.


-- | A remote object.
data Remote


-- | Construct a remote tracker.
foreign import ccall "makeTracker"
  makeTracker :: CString -> FunPtr PositionCallback' -> FunPtr VelocityCallback' -> FunPtr AccelerationCallback' -> IO (Ptr Remote)


-- | Run the main loop of a remote tracker.
foreign import ccall "mainloopTracker"
  mainloopTracker :: Ptr Remote -> IO ()


-- | Destroy a remote tracker.
foreign import ccall "deleteTracker"
  deleteTracker :: Ptr Remote -> IO ()


-- | Construct a remote button.
foreign import ccall "makeButton"
  makeButton :: CString -> FunPtr ButtonCallback' -> IO (Ptr Remote)


-- | Run the main loop of a remote button.
foreign import ccall "mainloopButton"
  mainloopButton :: Ptr Remote -> IO ()


-- | Destory a remote button.
foreign import ccall "deleteButton"
  deleteButton :: Ptr Remote -> IO ()


-- | Construct a remote analog.
foreign import ccall "makeAnalog"
  makeAnalog :: CString -> FunPtr AnalogCallback' -> IO (Ptr Remote)


-- | Run the main loop of a remote analog.
foreign import ccall "mainloopAnalog"
  mainloopAnalog :: Ptr Remote -> IO ()


-- | Destroy a remote analog.
foreign import ccall "deleteAnalog"
  deleteAnalog :: Ptr Remote -> IO ()


-- | Construction a remote dial.
foreign import ccall "makeDial"
  makeDial :: CString -> FunPtr DialCallback' -> IO (Ptr Remote)


-- | Run the main loop of a remote dial.
foreign import ccall "mainloopDial"
  mainloopDial :: Ptr Remote -> IO ()


-- | Destory a remote dial.
foreign import ccall "deleteDial"
  deleteDial :: Ptr Remote -> IO ()


-- | Sleep for the specified milliseconds.
foreign import ccall "vrpnSleep"
  vrpnSleep :: CDouble -> IO ()


-- | Sleep for the specified amount of time.
sleep :: RealFloat a => a     -- ^ The number of milliseconds.
                     -> IO () -- ^ An action to sleep the specified amount of time
sleep = vrpnSleep . realToFrac


-- | A remote VRPN device.
newtype RemoteDevice = RemoteDevice (ForeignPtr Remote, ForeignPtr Remote -> IO ())


-- | Open a remote VRPN device.
openDevice :: (Enum s, Enum b, Enum d, RealFloat a)
           => Device s b d a                -- ^ The device.
           -> IO RemoteDevice               -- ^ An action for opening the device.
openDevice Tracker{..} =
  do
    positionCallback'     <- maybe (return nullFunPtr) (wrapPositionCallback     . makePositionCallback    ) positionCallback
    velocityCallback'     <- maybe (return nullFunPtr) (wrapVelocityCallback     . makeVelocityCallback    ) velocityCallback
    accelerationCallback' <- maybe (return nullFunPtr) (wrapAccelerationCallback . makeAccelerationCallback) accelerationCallback
    ptr <-
      withCString device $ \device' ->
        makeTracker device' positionCallback' velocityCallback' accelerationCallback'
    ptr' <-
      newForeignPtr ptr $ do
        deleteTracker ptr
        freeHaskellFunPtr positionCallback'
        freeHaskellFunPtr velocityCallback'
        freeHaskellFunPtr accelerationCallback'
    return $ RemoteDevice (ptr', flip withForeignPtr mainloopTracker)
openDevice Button{..} =
  do
    buttonCallback' <- maybe (return nullFunPtr) (wrapButtonCallback . makeButtonCallback) buttonCallback
    ptr <-
      withCString device $ \device' ->
        makeButton device' buttonCallback'
    ptr' <-
      newForeignPtr ptr $ do
        deleteButton ptr
        freeHaskellFunPtr buttonCallback'
    return $ RemoteDevice (ptr', flip withForeignPtr mainloopButton)
openDevice Analog{..} =
  do
    analogCallback' <- maybe (return nullFunPtr) (wrapAnalogCallback . makeAnalogCallback) analogCallback
    ptr <-
      withCString device $ \device' ->
        makeAnalog device' analogCallback'
    ptr' <-
      newForeignPtr ptr $ do
        deleteAnalog ptr
        freeHaskellFunPtr analogCallback'
    return $ RemoteDevice (ptr', flip withForeignPtr mainloopAnalog)
openDevice Dial{..} =
  do
    dialCallback' <- maybe (return nullFunPtr) (wrapDialCallback . makeDialCallback) dialCallback
    ptr <-
      withCString device $ \device' ->
        makeDial device' dialCallback'
    ptr' <-
      newForeignPtr ptr $ do
        deleteDial ptr
        freeHaskellFunPtr dialCallback'
    return $ RemoteDevice (ptr', flip withForeignPtr mainloopDial)


-- | Close a remote device.
closeDevice :: RemoteDevice -- ^ The device.
            -> IO ()        -- ^ An action for closing the device.
closeDevice (RemoteDevice (device, _)) = finalizeForeignPtr device


-- | Run the main loop of a device *once*.
mainLoop :: RemoteDevice -- ^ The device.
         -> IO ()        -- ^ An action for running the main loop of the device *once*.
mainLoop (RemoteDevice (device, mainloopDevice)) = mainloopDevice device


-- | Run the main loops of devices *repeatedly*.
mainLoops :: RealFloat a
          => ExitCallback   -- ^ Callback for exiting the loop.
          -> a              -- ^ The number of milliseconds to idle after each device's main loop is run once.
          -> [RemoteDevice] -- ^ The devices.
          -> IO ()          -- ^ An action for running the main loops *repeatedly*.
mainLoops exitCallback milliseconds devices =
  do
    mapM_ mainLoop devices
    sleep milliseconds
    exit <- exitCallback
    unless exit
      $ mainLoops exitCallback milliseconds devices