packages feed

switch-0.1.0.0: src/Device/Nintendo/Switch/Controller.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE KindSignatures      #-}
module Device.Nintendo.Switch.Controller where

-- attoparsec
import Data.Attoparsec.ByteString (IResult(Done), Parser, anyWord8, parse, take, word8)

-- base
import Data.Bits      ((.&.), (.|.), Bits, shiftL, shiftR)
import Data.Int       (Int16)
import Data.IORef     (IORef)
import Data.Word      (Word8, Word16)
import Prelude hiding (take)

-- bytestring
import Data.ByteString (ByteString)

-- hidapi
import System.HIDAPI (Device)

-- switch
import Device.Nintendo.Switch.Utils (tripleParser, tripleZipWith)

-- | The types of Nintendo Switch controllers that are currently supported by
-- this library.
--
-- Note that this type is mostly used on the type level (using @DataKinds@)
-- in order to prevent programming mistakes at compile-time (e.g., to prevent
-- sending a rumble command to a controller which has no rumble feature).
--
-- Chances are very high that you don't need this type on the value level, but
-- rather on the type level, for example via @TypeApplications@
-- (see 'Device.Nintendo.Switch.getControllerInfos').
data ControllerType
  = LeftJoyCon
  | RightJoyCon
  | ProController
  deriving (Eq, Read, Show)

-- | A handle which represents a connected Nintendo Switch controller.
data Controller (t :: ControllerType) =
  Controller
    { handle      :: Device
    , counter     :: IORef Word8
    , calibration :: Calibration
    }

data Calibration =
  Calibration
    { accCoeffs     :: (Float, Float, Float)
    , gyroCoeffs    :: (Float, Float, Float)
    , leftStickCal  :: StickCalibration
    , rightStickCal :: StickCalibration
    }
  deriving (Eq, Read, Show)

data RawSensorCalibration =
  RawSensorCalibration
    { rawAcc             :: (Int16, Int16, Int16)
    , rawAccSensitivity  :: (Int16, Int16, Int16)
    , rawGyro            :: (Int16, Int16, Int16)
    , rawGyroSensitivity :: (Int16, Int16, Int16)
    }
  deriving (Eq, Read, Show)

data RawStickCalibration =
  RawStickCalibration
    { rawMinusX  :: Word16
    , rawCenterX :: Word16
    , rawPlusX   :: Word16
    , rawMinusY  :: Word16
    , rawCenterY :: Word16
    , rawPlusY   :: Word16
    }
  deriving (Eq, Read, Show)

data RawCalibration =
  RawCalibration
    { rawLeftStickCal  :: RawStickCalibration
    , rawRightStickCal :: RawStickCalibration
    , rawSensorCal     :: RawSensorCalibration
    }
  deriving (Eq, Read, Show)

data StickCalibration =
  StickCalibration
    { deadCenter :: Float
    , deadOuter  :: Float
    , minusX     :: Float
    , centerX    :: Float
    , plusX      :: Float
    , minusY     :: Float
    , centerY    :: Float
    , plusY      :: Float
    }
  deriving (Eq, Read, Show)

-- | A constraint which indicates that a Nintendo Switch controller is able to
-- turn portions of its internal flash memory into valid calibration information.
class HasCalibration (t :: ControllerType) where
  calibrate :: RawCalibration -> Calibration

instance HasCalibration 'LeftJoyCon where
  calibrate (RawCalibration l r s) =
    Calibration
      { accCoeffs     = tripleZipWith toAccCoeff (rawAcc s) (rawAccSensitivity s)
      , gyroCoeffs    = tripleZipWith toGyroCoeff (rawGyro s) (rawGyroSensitivity s)
      , leftStickCal  = toStickCal 0.15 0.10 l
      , rightStickCal = toStickCal 0.15 0.10 r
      }

instance HasCalibration 'RightJoyCon where
  calibrate (RawCalibration l r s) =
    Calibration
      { accCoeffs     = tripleZipWith toAccCoeff (rawAcc s) (rawAccSensitivity s)
      , gyroCoeffs    = tripleZipWith toGyroCoeff (rawGyro s) (rawGyroSensitivity s)
      , leftStickCal  = toStickCal 0.15 0.10 l
      , rightStickCal = toStickCal 0.15 0.10 r
      }

instance HasCalibration 'ProController where
  calibrate (RawCalibration l r s) =
    Calibration
      { accCoeffs     = tripleZipWith toAccCoeff (rawAcc s) (rawAccSensitivity s)
      , gyroCoeffs    = tripleZipWith toGyroCoeff (rawGyro s) (rawGyroSensitivity s)
      , leftStickCal  = toStickCal 0.10 0.10 l
      , rightStickCal = toStickCal 0.10 0.10 r
      }

toAccCoeff :: Int16 -> Int16 -> Float
toAccCoeff sense value =
  (1 / (fromIntegral sense - fromIntegral value)) * 4 -- in Gs

toGyroCoeff :: Int16 -> Int16 -> Float
toGyroCoeff sense value =
  (936 / (fromIntegral sense - fromIntegral value)) * (pi / 180) -- in radians per second

toStickCal :: Float -> Float -> RawStickCalibration -> StickCalibration
toStickCal dc dout raw =
  StickCalibration
    { deadCenter = dc
    , deadOuter  = dout
    , minusX     = fromIntegral $ rawMinusX raw
    , centerX    = fromIntegral $ rawCenterX raw
    , plusX      = fromIntegral $ rawPlusX raw
    , minusY     = fromIntegral $ rawMinusY raw
    , centerY    = fromIntegral $ rawCenterY raw
    , plusY      = fromIntegral $ rawPlusY raw
    }

axisCalibrationParser :: Parser RawSensorCalibration
axisCalibrationParser = do
  acc       <- tripleParser
  accSense  <- tripleParser
  gyro      <- tripleParser
  gyroSense <- tripleParser
  pure $
    RawSensorCalibration
      { rawAcc             = acc
      , rawAccSensitivity  = accSense
      , rawGyro            = gyro
      , rawGyroSensitivity = gyroSense
      }

axisUserParser :: Parser RawSensorCalibration
axisUserParser = do
  _ <- word8 0xB2
  _ <- word8 0xA1
  axisCalibrationParser

parseStickUserCalibration :: ByteString -> (Maybe RawStickCalibration, Maybe RawStickCalibration)
parseStickUserCalibration string =
  case parse stickUserParser string of
    Done _ result -> result
    _             -> (Nothing, Nothing)

stickCalibrationParser :: (Bits a, Num a) => Parser (a, a, a, a, a, a)
stickCalibrationParser = do
  byte0 <- fromIntegral <$> anyWord8
  byte1 <- fromIntegral <$> anyWord8
  byte2 <- fromIntegral <$> anyWord8
  byte3 <- fromIntegral <$> anyWord8
  byte4 <- fromIntegral <$> anyWord8
  byte5 <- fromIntegral <$> anyWord8
  byte6 <- fromIntegral <$> anyWord8
  byte7 <- fromIntegral <$> anyWord8
  byte8 <- fromIntegral <$> anyWord8
  let d0 = shiftL byte1 8 .&. 0x0F00 .|. byte0
      d1 = shiftL byte2 4 .|. shiftR byte1 4
      d2 = shiftL byte4 8 .&. 0x0F00 .|. byte3
      d3 = shiftL byte5 4 .|. shiftR byte4 4
      d4 = shiftL byte7 8 .&. 0x0F00 .|. byte6
      d5 = shiftL byte8 4 .|. shiftR byte7 4
  pure (d0, d1, d2, d3, d4, d5)

leftStickCalibrationParser :: Parser RawStickCalibration
leftStickCalibrationParser = do
  (d0, d1, d2, d3, d4, d5) <- stickCalibrationParser
  pure $
    RawStickCalibration
      { rawMinusX  = d2 - d4
      , rawCenterX = d2
      , rawPlusX   = d2 + d0
      , rawMinusY  = d3 - d5
      , rawCenterY = d3
      , rawPlusY   = d3 + d1
      }

rightStickCalibrationParser :: Parser RawStickCalibration
rightStickCalibrationParser = do
  (d0, d1, d2, d3, d4, d5) <- stickCalibrationParser
  pure $
    RawStickCalibration
      { rawMinusX  = d0 - d2
      , rawCenterX = d0
      , rawPlusX   = d0 + d4
      , rawMinusY  = d1 - d3
      , rawCenterY = d1
      , rawPlusY   = d1 + d5
      }

stickFactoryParser :: Parser (RawStickCalibration, RawStickCalibration)
stickFactoryParser = do
  leftCal <- leftStickCalibrationParser
  rightCal <- rightStickCalibrationParser
  pure (leftCal, rightCal)

stickUserParser :: Parser (Maybe RawStickCalibration, Maybe RawStickCalibration)
stickUserParser = do
  magicL0 <- anyWord8
  magicL1 <- anyWord8
  ls <- if magicL0 == 0xB2 && magicL1 == 0xA1
        then Just <$> leftStickCalibrationParser
        else const Nothing <$> take 9
  magicR0 <- anyWord8
  magicR1 <- anyWord8
  rs <- if magicR0 == 0xB2 && magicR1 == 0xA1
        then Just <$> rightStickCalibrationParser
        else pure Nothing
  pure (ls, rs)