packages feed

ble-0.4.0.0: src/Bluetooth/Internal/DBus.hs

module Bluetooth.Internal.DBus where

import Control.Monad.Except
import Control.Monad.Reader
import Data.IORef           (readIORef, writeIORef)
import Data.Monoid          ((<>))
import DBus
import DBus.Signal          (execSignalT)
import Lens.Micro

import qualified Data.ByteString as BS
import qualified Data.Map        as Map
import qualified Data.Text       as T

import Bluetooth.Internal.Errors
import Bluetooth.Internal.HasInterface
import Bluetooth.Internal.Interfaces
import Bluetooth.Internal.Lenses
import Bluetooth.Internal.Types
import Bluetooth.Internal.Utils


-- | Registers an application and advertises it. If you would like to have
-- finer-grained control of the advertisement, use @registerApplication@ and
-- @advertise@.
registerAndAdvertiseApplication :: Application -> BluetoothM ApplicationRegistered
registerAndAdvertiseApplication app = do
  reg <- registerApplication app
  advertise (advertisementFor app)
  return reg

-- | Registers an application (set of services) with Bluez.
registerApplication :: Application -> BluetoothM ApplicationRegistered
registerApplication app = do
  conn <- ask
  addAllObjs conn app
  bluezPath' <- liftIO $ readIORef bluezPath
  () <- callMethodBM bluezPath' gattManagerIFace "RegisterApplication" args
  return $ ApplicationRegistered (app ^. path)
  where
    args :: (ObjectPath, Map.Map T.Text Any)
    args = (app ^. path, Map.empty)

unregisterApplication :: ApplicationRegistered -> BluetoothM ()
unregisterApplication (ApplicationRegistered appPath) = do
  bluezPath' <- liftIO $ readIORef bluezPath
  callMethodBM bluezPath' gattManagerIFace "UnregisterApplication" appPath


-- | Adds handlers for all the objects managed by the Application (plus the
-- Application itself).
addAllObjs :: Connection -> Application -> BluetoothM ()
addAllObjs conn app = do
  liftIO $ addObject conn (app ^. path) (app `withInterface` objectManagerIFaceP)
  liftIO $ forM_ (zip [0..] (app ^. services)) $ \(i,s) -> do
    let p = serviceObjectPath (app ^. path) i
    addObject conn p
      $  (WOP p s `withInterface` gattServiceIFaceP)
      <> (WOP p s `withInterface` propertiesIFaceP)
    registerObjectPath (s ^. uuid) p
    forM_ (zip [0..] (s ^. characteristics)) $ \(i', c) -> do
      let p' = characteristicObjectPath p i'
      addObject conn p'
        $ (WOP p' c `withInterface` gattCharacteristicIFaceP)
       <> (WOP p' c `withInterface` propertiesIFaceP)
      registerObjectPath (c ^. uuid) p'
   where
     registerObjectPath :: UUID -> ObjectPath -> IO ()
     registerObjectPath uuid' op = writeIORef (objectPathOf uuid') (Just op)

-- | Advertise a set of services.
advertise :: WithObjectPath Advertisement -> BluetoothM ()
advertise adv = do
  conn <- ask
  liftIO $ do
    addObject conn (adv ^. path)
      $  (adv `withInterface` leAdvertisementIFaceP)
      <> ((adv ^. value) `withInterface` propertiesIFaceP)
  bluezPath' <- liftIO $ readIORef bluezPath
  callMethodBM bluezPath' leAdvertisingManagerIFace "RegisterAdvertisement" args
  where
    args :: (ObjectPath, Map.Map T.Text Any)
    args = (adv ^. path, Map.empty)

-- | Unregister an adverstisement.
unadvertise :: WithObjectPath Advertisement -> BluetoothM ()
unadvertise adv = do
  bluezPath' <- liftIO $ readIORef bluezPath
  callMethodBM bluezPath' leAdvertisingManagerIFace "UnregisterAdvertisement" args
  where
    args :: ObjectPath
    args = adv ^. path


-- | Create an advertisement for all of an application's services.
-- The advertisement will be for peripheral (not broadcast) by default.
advertisementFor :: Application -> WithObjectPath Advertisement
advertisementFor app = WOP p adv
  where
    adv = def & serviceUUIDs .~ (app ^.. services . traversed . uuid)
    p = app ^. path & toText %~ (</> "adv")


-- | Triggers notifications or indications.
triggerNotification :: ApplicationRegistered -> CharacteristicBS 'Local -> BluetoothM ()
triggerNotification (ApplicationRegistered _) c = do
   case c ^. readValue of
     Nothing -> throwError "Handler does not have a readValue implementation!"
     Just readHandler -> do
       res' <- liftIO $ runHandler readHandler
       res <- case res' of
         Left e -> throwError $ BLEError e
         Right v -> return v
       mPath <- liftIO $ readIORef $ objectPathOf (c ^. uuid)
       case mPath of
         Nothing -> throwError "UUID not found - are you sure you registered the application containing it?"
         Just path' -> runEff $ propertyChanged (valProp $ WOP path' c) res
  where
    runEff :: MethodHandlerT IO x -> BluetoothM x
    runEff act = do
      conn <- asks dbusConn
      res <- liftIO $ execSignalT act conn
      case res of
        Left e -> throwError . DBusError . MethodErrorMessage $ errorBody e
        Right val -> return val

-- | Get a service by UUID. Returns Nothing if the service could not be found.
getService :: UUID -> BluetoothM (Maybe (Service 'Remote))
getService serviceUUID = do
  services' <- getAllServices
  return $ case filter (\x -> x ^. uuid == serviceUUID) services' of
    [] -> Nothing
    -- This should never be a list with more than one element.
    (x:_) -> Just x

-- | Get all registered services.
getAllServices :: BluetoothM [Service 'Remote]
getAllServices = do
  objects :: Map.Map ObjectPath (Map.Map T.Text (Map.Map T.Text DontCareFromRep))
    <- callMethodBM "/" objectManagerIFace "GetManagedObjects" ()

  -- We need to construct services manually, since we get the characteristics
  -- separately.
  let chars =
       Map.keys $ Map.filter (T.pack gattCharacteristicIFace `Map.member`) objects
  let servs =
       Map.keys $ Map.filter (T.pack gattServiceIFace `Map.member`) objects
  forM servs $ \s -> mkService s [ c | c <- chars , s  `isPathPrefix` c ]

  where

    readProps :: [CharacteristicProperty]
    readProps = [CPRead, CPEncryptRead, CPEncryptAuthenticatedRead]

    writeProps :: [CharacteristicProperty]
    writeProps = [ CPWrite, CPWriteWithoutResponse, CPEncryptWrite
                 , CPEncryptAuthenticatedRead, CPAuthenticatedSignedWrites]

    charFromRep :: ObjectPath -> DBusValue AnyDBusDict
      -> Maybe (CharacteristicBS 'Remote)
    charFromRep charPath dict' = do
      dict :: Map.Map T.Text (DBusValue 'TypeVariant) <- fromRep dict'
      let unmakeAny :: (Representable a) => DBusValue 'TypeVariant -> Maybe a
          unmakeAny x = fromRep =<< fromVariant x
      uuid' :: UUID <- unmakeAny =<< Map.lookup "UUID" dict
      properties' <- unmakeAny =<< Map.lookup "Flags" dict
      let mrv = if any (`elem` readProps) properties'
            then Just $
              callMethodBM charPath gattCharacteristicIFace "ReadValue" ()
            else Nothing
      let mwv = if any (`elem` writeProps) properties'
            then Just $
              callMethodBM charPath gattCharacteristicIFace "WriteValue"
            else Nothing
      let char = RemoteChar uuid' properties' mrv mwv charPath
      return char

    mkChar :: ObjectPath -> BluetoothM (CharacteristicBS 'Remote)
    mkChar charPath = do
      charProps  <- callMethodBM charPath propertiesIFace "GetAll"
        (T.pack gattCharacteristicIFace)
      case charFromRep charPath charProps of
        Nothing -> throwError $ OtherError
          "Bluez returned invalid characteristic"
        Just c -> return c

    serviceFromRep :: DBusValue AnyDBusDict -> Maybe (Service m)
    serviceFromRep dict' = do
      dict :: Map.Map T.Text (DBusValue 'TypeVariant) <- fromRep dict'
      let unmakeAny :: (Representable a) => DBusValue 'TypeVariant -> Maybe a
          unmakeAny x = fromRep =<< fromVariant x
      uuid' :: UUID <- unmakeAny =<< Map.lookup "UUID" dict
      return $ Service uuid' []

    mkService :: ObjectPath -> [ObjectPath] -> BluetoothM (Service 'Remote)
    mkService servicePath charPaths = do
      serviceProps <- callMethodBM servicePath propertiesIFace "GetAll"
        (T.pack gattServiceIFace)
      chars <- mapM mkChar charPaths
      case serviceFromRep serviceProps of
        Nothing -> throwError $ OtherError
          "Bluez returned invalid service"
        Just serv -> return $ serv & characteristics .~ chars

-- | Starts notifications on the remote (peripheral), handling it with the
-- provided callback.
startNotify ::
  {-(Representable a, ArgParity (FlattenRepType (RepType a)) ~ 'Arg 'Null)-}
  CharacteristicBS 'Remote -> (BS.ByteString -> IO ()) -> BluetoothM ()
startNotify char handler = do
  conn <- dbusConn <$> ask
  rprop <- liftIO $ valRemoteProp char
  liftIO $ handlePropertyChanged rprop (whenJust handler) conn
  callMethodBM (char ^. path) gattCharacteristicIFace "StartNotify" ()
  where
    whenJust fn (Just v) = fn v
    whenJust _  Nothing  = return ()

-- | Stop notifications on the remote (peripheral).
stopNotify :: Characteristic 'Remote a -> BluetoothM ()
stopNotify char
  = callMethodBM (char ^. path) gattCharacteristicIFace "StopNotify" ()

-- Helpers

valRemoteProp :: CharacteristicBS 'Remote -> IO (RemoteProperty (RepType BS.ByteString))
valRemoteProp char = do
  bluezName' <- readIORef bluezName
  return $ RP
     { rpEntity = bluezName' -- Is this the right entity?
     , rpObject = char ^. path
     , rpInterface = T.pack gattCharacteristicIFace
     , rpName = "Value"
     }

callMethodBM :: (Representable args, Representable ret)
  => ObjectPath
  -> String
  -> T.Text
  -> args
  -> BluetoothM ret
callMethodBM opath iface methodName args = do
  conn <- ask
  bluezName' <- liftIO $ readIORef bluezName
  toBluetoothM . const $ callMethod bluezName' opath (T.pack iface) methodName args [] (dbusConn conn)