switch-0.1.0.0: src/Device/Nintendo/Switch/Connection.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Device.Nintendo.Switch.Connection where
-- attoparsec
import Data.Attoparsec.ByteString (Parser, maybeResult, parse)
-- base
import Control.Exception (Exception, bracket, throwIO)
import Data.IORef (newIORef)
import Prelude hiding (init)
-- bytestring
import Data.ByteString (ByteString)
-- switch
import Device.Nintendo.Switch.Controller (Controller(..), ControllerType(..),
HasCalibration(..), RawCalibration(..),
axisCalibrationParser, stickFactoryParser,
parseStickUserCalibration, axisUserParser)
import Device.Nintendo.Switch.Input (Acknowledgement(ACK), ReplyData(SetInputMode),
withCommandReply, withRawSPIData)
import Device.Nintendo.Switch.Output (InputMode(Standard), setInputModeInternal)
import qualified System.HIDAPI as HID
-- | A handle which represents a virtual Nintendo Switch console. The handle
-- is used to detect controllers and manage their connections.
data Console = Console
deriving (Eq, Ord, Read, Show)
-- | Initializes a Nintendo Switch console handle. In other words, it lets us
-- pretend to be Nintendo Switch console in order to detect controllers and manage
-- their connections. You must call this first before doing anything else.
init :: IO Console
init = HID.init >> pure Console
-- | Destroys a virtual Nintendo Switch handle. You must call this when you are
-- finished talking to the controllers.
exit :: Console -> IO ()
exit Console = HID.exit
-- | A convenient wrapper around 'init' and 'exit'.
withConsole :: (Console -> IO a) -> IO a
withConsole = bracket init exit
-- | A handle which represents an unconnected Nintendo Switch controller.
newtype ControllerInfo (t :: ControllerType) =
ControllerInfo HID.DeviceInfo
deriving Show
-- | A constraint which indicates that a controller is a valid Nintendo Switch
-- controller that can be detected and connected to.
class IsController (t :: ControllerType) where
productID :: HID.ProductID
vendorID :: HID.VendorID
vendorID = 0x057E -- standard: Nintendo
instance IsController 'LeftJoyCon where
productID = 0x2006
instance IsController 'RightJoyCon where
productID = 0x2007
instance IsController 'ProController where
productID = 0x2009
-- | A 'ConnectionException' is thrown if something goes wrong when reading the
-- internal data of a Nintendo Switch controller when connecting to it. This
-- should not occur if you have an unmodified controller (i.e., you have not
-- tampered with its internal SPI flash memory).
data ConnectionException
= NoFactoryStickException -- ^ Indicates that a controller has no factory stick calibration.
| NoFactoryAxisException -- ^ Indicates that a controller has no factory sensor calibration.
deriving Eq
instance Exception ConnectionException
instance Show ConnectionException where
show NoFactoryStickException = "Could not determine the factory stick calibration."
show NoFactoryAxisException = "Could not determine the factory sensor calibration."
-- | Connects to a detected Nintendo Switch controller.
--
-- Can throw a 'ConnectionException' if something is very wrong with your
-- internal controller memory (i.e., if you have tampered with it).
connect
:: forall t . HasCalibration t
=> ControllerInfo t -- ^ The detected Nintendo Switch controller.
-> IO (Controller t) -- ^ The connected Nintendo Switch controller.
connect (ControllerInfo devInfo) = do
dev <- HID.openDeviceInfo devInfo
ref <- newIORef 0
facStick <- withRawSPIData dev ref 0x603D 18 (tryParse stickFactoryParser)
facAxis <- withRawSPIData dev ref 0x6020 24 (tryParse axisCalibrationParser)
(usl,usr) <- withRawSPIData dev ref 0x8010 22 parseStickUserCalibration
usrAxis <- withRawSPIData dev ref 0x8026 26 (tryParse axisUserParser)
controller <-
case (facStick, facAxis) of
(Nothing, _) -> throwIO NoFactoryStickException
(_, Nothing) -> throwIO NoFactoryAxisException
(Just (fsl,fsr), Just sensor) ->
let ls = maybe fsl id usl
rs = maybe fsr id usr
ax = maybe sensor id usrAxis
cal = calibrate @t (RawCalibration ls rs ax) in
pure $ Controller dev ref cal
setInputModeInternal Standard controller
withCommandReply 10 50 controller $ \case
SetInputMode (ACK ()) -> Just ()
_ -> Nothing
pure controller
where
tryParse :: Parser a -> ByteString -> Maybe a
tryParse parser = maybeResult . parse parser
-- | Disconnects a Nintendo Switch controller. You must not use the controller
-- handle after disconnecting.
disconnect :: Controller t -> IO ()
disconnect = HID.close . handle
-- | A convenient wrapper around 'connect' and 'disconnect'.
withController :: HasCalibration t => ControllerInfo t -> (Controller t -> IO a) -> IO a
withController info =
bracket
( connect info )
( disconnect )
-- | Detects all Nintendo Switch controllers of a specific 'ControllerType',
-- usually connected via Bluetooth.
--
-- You may want to use this function with @TypeApplications@ if the controller
-- type cannot be inferred, like:
--
-- @
-- 'getControllerInfos' \@''LeftJoyCon' console
-- @
--
getControllerInfos :: forall t . IsController t => Console -> IO [ControllerInfo t]
getControllerInfos Console =
let
vendID = vendorID @t
prodID = productID @t
in do
devInfos <- HID.enumerate (Just vendID) (Just prodID)
pure $ ControllerInfo <$> devInfos