packages feed

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

{-# LANGUAGE DataKinds  #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PolyKinds  #-}
module Device.Nintendo.Switch.Output where

-- base
import Control.Exception (Exception, throwIO)
import Control.Monad     (join, when)
import Data.Bits         ((.|.), (.&.), shiftL, shiftR)
import Data.IORef        (IORef, readIORef, writeIORef)
import Data.Word         (Word8, Word32)

-- bytestring
import qualified Data.ByteString as BS

-- hidapi
import System.HIDAPI (Device, write)

-- switch
import Device.Nintendo.Switch.Controller (Controller(..), ControllerType(..))
import Device.Nintendo.Switch.Utils      (clamp, combine, discretize, pairs)

-- | A constraint which indicates that a Nintendo Switch controller has a home
-- light (see 'setHomeLight').
class HasHomeLight t

instance HasHomeLight 'RightJoyCon
instance HasHomeLight 'ProController

-- | A constraint which indicates that a Nintendo Switch controller has a left-side
-- rumble unit (see 'setLeftRumble').
class HasLeftRumble t

instance HasLeftRumble 'LeftJoyCon
instance HasLeftRumble 'ProController

-- | A constraint which indicates that a Nintendo Switch controller has a right-side
-- rumble unit (see 'setRightRumble').
class HasRightRumble t

instance HasRightRumble 'RightJoyCon
instance HasRightRumble 'ProController

-- | A constraint which indicates that a Nintendo Switch controller has player lights
-- (i.e., the four LEDs which represent the player number; see 'setPlayerLights').
class HasPlayerLights t

instance HasPlayerLights 'LeftJoyCon
instance HasPlayerLights 'RightJoyCon
instance HasPlayerLights 'ProController

-- | A constraint which indicates that a Nintendo Switch controller supports multiple
-- input modes (see 'setInputMode').
class HasInputMode t

instance HasInputMode 'LeftJoyCon
instance HasInputMode 'RightJoyCon

-- | An 'OutputException' is thrown if something goes wrong when sending commands to
-- a Nintendo Switch controller.
data OutputException = WriteException

instance Exception OutputException
instance Show OutputException where
  show WriteException = "Could not send all data to the controller device."

-- | The base duration of a home light configuration in milliseconds. It will
-- always be limited to an interval between 8ms and 175ms. It is called base
-- duration because it will be multiplied with other factors in order to obtain
-- the overall durations of fadings within home light configurations.
type BaseDuration = Word8

-- | The LED intensity of the home light. It will always be limited to an interval
-- between 0 and 100.
type Intensity = Word8

-- | The fade duration factor of the home light. It will always be limited to an
-- interval between 0 and 15 and is multiplied with the 'BaseDuration' to obtain
-- the overall fade duration in milliseconds.
type FadeFactor = Word8

-- | The light duration factor of the home light. It will always be limited to an
-- interval between 0 and 15 and is multiplied with the 'BaseDuration' to obtain
-- the overall light duration in milliseconds.
type LightFactor = Word8

-- | A home light cycle consists of a target LED intensity, a fade factor which
-- controls the time needed to reach that LED intensity, and a light factor which
-- controls how long to keep the target LED intensity up.
type CycleConfig = (Intensity, FadeFactor, LightFactor)

-- | Defines the repeat behaviour after all the home light configuration cycles
-- have ended.
data RepeatBehaviour
  = Forever
    -- ^ Repeat the configured home light configuration cycles forever.
  | Times Word8
    -- ^ Repeat the configured home light configuration cycles a specific amount
    -- of times. It will always be limited to an interval between 1 and 15.
  deriving (Eq, Read, Show)

-- | The home light of a Nintendo Switch Controller can be controlled using repeatable
-- configuration cycles. See 'endlessPulse' for an example configuration.
data HomeLightConfig
  = Off
    -- ^ Turn off the home light.
  | Once BaseDuration Intensity CycleConfig
    -- ^ Given a start intensity of the home light LED, fade to a target LED
    -- intensity in a given time, and then keep this LED intensity up for a given
    -- amount of time.
    --
    -- The fade duration in milliseconds is calculated by multiplying the 'BaseDuration'
    -- with the 'FadeFactor' of the 'CycleConfig'. The light upkeep duration in
    -- milliseconds is calculated by multiplying the 'BaseDuration' with the 'LightFactor'
    -- of the 'CycleConfig'.
    --
    -- Example - fade from a switched off LED (@0@) to a fully bright LED (@100@) in 500ms
    -- (@50@ms * @10@), stay there for one second (@50@ms * @20@), then turn it off:
    --
    -- @
    --     'Once' 50 0 (100, 10, 20)
    -- @
  | Cyclic BaseDuration Intensity [CycleConfig] RepeatBehaviour
    -- ^ Given a start intensity of the home light LED, repeatedly fade to a
    -- target LED intensity in a given time, and then keep this LED intensity up
    -- for a given amount of time. The fade durations and light upkeep durations
    -- are calculated per cycle configuration as described for 'Once'. See
    -- 'endlessPulse' for a cyclic configuration example.
  deriving (Eq, Read, Show)

lightConfigCommand :: HomeLightConfig -> [Word8]
lightConfigCommand = \case
  Off ->
    replicate 25 0x00
  Once dur int (cycInt, fade, cycDur) ->
    let
      byte0  = scaleDuration dur
      byte1  = shiftL (scaleIntensity int) 4
      byte2  = shiftL (scaleIntensity cycInt) 4
      byte3H = clampMultiplier fade
      byte3L = clampMultiplier cycDur
    in
      byte0 : byte1 : byte2 : combine byte3H byte3L : replicate 45 0x00
  Cyclic dur int cfgs rep ->
    let
      cycles = take 15 cfgs
      byte0H = length cycles
      byte0L = scaleDuration dur
      byte1H = scaleIntensity int
      byte1L = case rep of
        Forever -> 0x0
        Times n -> clamp 1 15 n
      padded = cycles ++ replicate (16 - byte0H) (0,0,0)
      pairBytes = fmap (uncurry pairCommand) $ pairs padded
    in
      combine (fromIntegral byte0H) byte0L
        : combine byte1H byte1L
        : join pairBytes
       ++ replicate 23 0x00
  where
    scaleIntensity   = discretize 0 100 0 15
    scaleDuration    = discretize 8 175 0 15
    clampMultiplier  = clamp 0 15
    
    pairCommand :: CycleConfig -> CycleConfig -> [Word8]
    pairCommand (int1, fade1, dur1) (int2, fade2, dur2) =
      let
        byte0H = scaleIntensity int1
        byte0L = scaleIntensity int2
        byte1H = clampMultiplier fade1
        byte1L = clampMultiplier dur1
        byte2H = clampMultiplier fade2
        byte2L = clampMultiplier dur2
      in
        [combine byte0H byte0L, combine byte1H byte1L, combine byte2H byte2L]

-- | A convenient home light configuration which pulsates the home light LED:
--
-- @
--     'Cyclic'
--       ( 100 )         -- Base duration factor is 100ms.
--       (   0 )         -- LED is turned off at the beginning (intensity 0).
--       [ (100, 5, 1)   -- Fade to LED intensity 100 in 500ms (100ms * 5) and stay there for 100ms (100ms * 1).
--       , (  0, 5, 1) ] -- Fade to LED intensity   0 in 500ms (100ms * 5) and stay there for 100ms (100ms * 1).
--       ( Forever )     -- Repeat these two cycles forever, thus generating a pulse-like LED.
-- @
endlessPulse :: HomeLightConfig
endlessPulse =
  Cyclic
    ( 100 )
    ( 0   )
    [ (100, 5, 1), (0, 5, 1) ]
    ( Forever )

-- | Sets the home light (i.e., the LED ring around the home button) of a Nintendo
-- Switch controller.
--
-- Note: After sending a command like this to a controller, it is highly advised
-- to check its corresponding 'Device.Nintendo.Switch.CommandReply'
-- ('Device.Nintendo.Switch.SetHomeLight', to be exact) or at least call
-- 'Device.Nintendo.Switch.getInput' once before sending another command to
-- that controller. The function 'Device.Nintendo.Switch.withCommandReply' is a
-- convenient way to wait for a specific command reply from the controller.
setHomeLight :: HasHomeLight t => HomeLightConfig -> Controller t -> IO ()
setHomeLight cfg controller =
  sendSubcommand controller 0x01 0x38 $
    lightConfigCommand cfg

-- | Enables ('True') or disables ('False') the inertial measurement unit (i.e., 
-- accelerometer, gyroscope) of a Nintendo Switch controller. Inertial measurement
-- is disabled by default.
--
-- Note: After sending a command like this to a controller, it is highly advised
-- to check its corresponding 'Device.Nintendo.Switch.CommandReply'
-- ('Device.Nintendo.Switch.SetInertialMeasurement', to be exact) or at least call
-- 'Device.Nintendo.Switch.getInput' once before sending another command to
-- that controller. The function 'Device.Nintendo.Switch.withCommandReply' is a
-- convenient way to wait for a specific command reply from the controller.
setInertialMeasurement :: Bool -> Controller t -> IO ()
setInertialMeasurement on controller =
  sendSubcommand controller 0x01 0x40 $
    (if on then 0x01 else 0x00) : replicate 48 0x00

-- | The input mode of a Nintendo Switch controller determines the frequency and
-- amount of information received by 'Device.Nintendo.Switch.getInput'.
data InputMode
  = Standard
    -- ^ The default input mode. In this mode, controllers push 'Device.Nintendo.Switch.Input'
    -- packages in a 60Hz (Joy-Con) or 120Hz (Pro Controller) frequency, including
    -- 'Device.Nintendo.Switch.battery' information, 'Device.Nintendo.Switch.Analog' stick
    -- directions ('Device.Nintendo.Switch.stickLeft', 'Device.Nintendo.Switch.stickRight')
    -- and 'Device.Nintendo.Switch.Inertial' sensor data ('Device.Nintendo.Switch.extras')
    -- if activated via 'setInertialMeasurement'.
  | Simple
    -- ^ A simple input mode where a controller only pushes its 'Device.Nintendo.Switch.Input'
    -- whenever a button is pressed or a 'Device.Nintendo.Switch.CommandReply' ('Device.Nintendo.Switch.extras')
    -- is sent. In this mode, controllers only send 'Device.Nintendo.Switch.Discrete' stick
    -- directions ('Device.Nintendo.Switch.stickLeft', 'Device.Nintendo.Switch.stickRight')
    -- and no inertial sensor data. Furthermore, 'Device.Nintendo.Switch.battery' information
    -- is only sent in combination with command replies.
  deriving (Eq, Read, Show)

-- | Sets the input mode of a Nintendo Switch controller.
--
-- Note: After sending a command like this to a controller, it is highly advised
-- to check its corresponding 'Device.Nintendo.Switch.CommandReply'
-- ('Device.Nintendo.Switch.SetInputMode', to be exact) or at least call
-- 'Device.Nintendo.Switch.getInput' once before sending another command to
-- that controller. The function 'Device.Nintendo.Switch.withCommandReply' is a
-- convenient way to wait for a specific command reply from the controller.
setInputMode :: HasInputMode t => InputMode -> Controller t -> IO ()
setInputMode = setInputModeInternal

setInputModeInternal :: InputMode -> Controller t -> IO ()
setInputModeInternal mode controller =
  sendSubcommand controller 0x01 0x03 $
    toByte mode : replicate 48 0x00
  where
    toByte = \case
      Standard -> 0x30
      Simple   -> 0x3F

neutralPartRumble :: [Word8]
neutralPartRumble = [0x00, 0x01, 0x40, 0x40]

neutralRumble :: [Word8]
neutralRumble = neutralPartRumble ++ neutralPartRumble

sendCommand :: Device -> Word8 -> [Word8] -> IO ()
sendCommand dev cmdID cmdData = do
  size <- write dev $ BS.pack (cmdID : cmdData)
  when (size <= 0) $ throwIO WriteException -- should check the size more precisely here

sendRawSubcommand :: Device -> IORef Word8 -> Word8 -> Word8 -> [Word8] -> IO ()
sendRawSubcommand dev ref cmdID subID subData = do
  count <- readIORef ref
  sendCommand dev cmdID $ count : neutralRumble ++ subID : subData
  writeIORef ref $ 0x0F .&. succ count

sendSubcommand :: Controller t -> Word8 -> Word8 -> [Word8] -> IO ()
sendSubcommand controller =
  sendRawSubcommand
    ( handle controller )
    ( counter controller )

encodeHF :: (Floating a, RealFrac a) => a -> (Word8, Word8)
encodeHF freq = (hfH, hfL)
  where clamped = clamp 81.75177 1252.572266 freq
        base = round $ logBase 2 (clamped * 0.1) * 32
        hf = (base - 0x60) * 4 :: Int
        hfH = fromIntegral $ hf .&. 0xFF
        hfL = fromIntegral $ shiftR hf 8 .&. 0xFF

encodeLF :: (Floating a, RealFrac a) => a -> Word8
encodeLF freq = base - 0x40
  where clamped = clamp 40.875885 626.286133 freq
        base = round $ logBase 2 (clamped * 0.1) * 32

encodeHA :: (Floating a, RealFrac a) => a -> Word8
encodeHA amp | clamped < 0.117 = round $ (base - 0x60) / (5 - (2 ** clamped)) - 1
             | clamped >= 0.23 = round $ (base - 0x60) * 2 - 0xF6
             | otherwise = round $ base - 0xBC
  where clamped = clamp 0.0 1.0 amp
        base = logBase 2 (clamped * 1000) * 32

encodeLA :: (Floating a, RealFrac a) => a -> (Word8, Word8)
encodeLA amp = (laH, laL)
  where encoded = encodeHA amp `div` 2
        isOdd = odd encoded
        laH = if isOdd then 0x80 else 0x00
        laL = 0x40 + (if isOdd then encoded - 1 else encoded) `div` 2

rumblePartCommand :: RumbleConfig -> [Word8]
rumblePartCommand cfg = let
    (hfH, hfL) = encodeHF $ highFrequency cfg
    ha         = encodeHA $ highAmplitude cfg
    lf         = encodeLF $ lowFrequency cfg
    (laH, laL) = encodeLA $ lowAmplitude cfg
  in [hfH, ha + hfL, lf + laH, laL]

-- | Nintendo Switch controllers have a HD rumble feature which allows
-- fine-grained control of rumble strengths and directions. As a consequence,
-- a rumble is not configured by a mere numeric value, but by two (high and low)
-- pairs of frequencies and amplitudes. This library constrains the value ranges
-- of frequencies and amplitudes in order to always obtain sane configurations.
-- However, sending extreme values for these pairs over an extended period of time
-- may still damage a controller, so experiment wisely with rather short rumbles.
--
-- For technical discussions and the meaning of these values, one can read
-- <https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/issues/11 this>,
-- for example. A sample rumble configuration is provided by 'normalRumble'.
data RumbleConfig =
  RumbleConfig
    { highFrequency :: Double
      -- ^ The high frequency. It will always be limited to an interval between
      -- 81.75177 Hz and 1252.572266 Hz.
    , highAmplitude :: Double
      -- ^ The high amplitude. It will always be limited to an interval between
      -- 0.0 and 1.0.
    , lowFrequency  :: Double
      -- ^ The low frequency. It will always be limited to an interval between
      -- 40.875885 Hz and 626.286133 Hz.
    , lowAmplitude  :: Double
      -- ^ The low amplitude. It will always be limited to an interval between
      -- 0.0 and 1.0.
    }
  deriving (Eq, Read, Show)

-- | A convenient rumble configuration indicating a medium rumble strength.
--
-- @
--     'RumbleConfig'
--       { 'highFrequency' = 800
--       , 'highAmplitude' = 0.5
--       , 'lowFrequency'  = 330
--       , 'lowAmplitude'  = 0.75
--       }
-- @
normalRumble :: RumbleConfig
normalRumble =
  RumbleConfig
    { highFrequency = 800
    , highAmplitude = 0.5
    , lowFrequency  = 330
    , lowAmplitude  = 0.75
    }

-- | A convenient rumble configuration indicating no rumble.
noRumble :: RumbleConfig
noRumble =
  RumbleConfig
    { highFrequency = 320
    , highAmplitude = 0
    , lowFrequency  = 160
    , lowAmplitude  = 0
    }

-- | Enables ('True') or disables ('False') the rumble feature of a Nintendo
-- Switch controller. The rumble feature is disabled by default.
--
-- Note: After sending a command like this to a controller, it is highly advised
-- to check its corresponding 'Device.Nintendo.Switch.CommandReply'
-- ('Device.Nintendo.Switch.SetVibration', to be exact) or at least call
-- 'Device.Nintendo.Switch.getInput' once before sending another command to
-- that controller. The function 'Device.Nintendo.Switch.withCommandReply' is a
-- convenient way to wait for a specific command reply from the controller.
setVibration :: Bool -> Controller t -> IO ()
setVibration on controller =
  sendSubcommand controller 0x01 0x48 $
    (if on then 0x01 else 0x00) : replicate 48 0x00

-- | Sets the left rumble of a Nintendo Switch controller.
setLeftRumble :: HasLeftRumble t => RumbleConfig -> Controller t -> IO ()
setLeftRumble cfg controller =
  sendRawRumbleCommand
    ( handle controller )
    ( rumblePartCommand cfg )
    ( neutralPartRumble )

-- | Sets the right rumble of a Nintendo Switch controller.
setRightRumble :: HasRightRumble t => RumbleConfig -> Controller t -> IO ()
setRightRumble cfg controller =
  sendRawRumbleCommand
    ( handle controller )
    ( neutralPartRumble )
    ( rumblePartCommand cfg )

-- | Sets both the left rumble and right rumble of a Nintendo Switch controller.
-- Note that this is more efficient than setting the left rumble and right rumble
-- separately via 'setLeftRumble' and 'setRightRumble'.
setRumble
  :: (HasLeftRumble t, HasRightRumble t)
  => RumbleConfig -- ^ The left-side rumble configuration.
  -> RumbleConfig -- ^ The right-side rumble configuration.
  -> Controller t -- ^ The controller which should rumble.
  -> IO ()
setRumble leftCfg rightCfg controller =
  sendRawRumbleCommand
    ( handle controller )
    ( rumblePartCommand leftCfg )
    ( rumblePartCommand rightCfg )

sendRawRumbleCommand :: Device -> [Word8] -> [Word8] -> IO ()
sendRawRumbleCommand dev left right =
  sendCommand dev 0x10 $ 0x00 : left ++ right ++ replicate 40 0x00

requestRawSPI :: Device -> IORef Word8 -> Word32 -> Word8 -> IO ()
requestRawSPI dev ref start len =
  let
    byte0 = fromIntegral $         start .&. 0x000000FF
    byte1 = fromIntegral $ shiftR (start .&. 0x0000FF00) 8
    byte2 = fromIntegral $ shiftR (start .&. 0x00FF0000) 16
    byte3 = fromIntegral $ shiftR (start .&. 0xFF000000) 24
  in
  sendRawSubcommand dev ref 0x01 0x10 $
    [byte0, byte1, byte2, byte3, clamp 0x00 0x1D len] ++ replicate 44 0x00

-- | Nintendo Switch controllers have four LEDs that can be used to indicate
-- various things, for example the player number or the Bluetooth pairing status.
-- The LEDs are numbered from left to right (i.e., 'led0' is the leftmost LED,
-- 'led3' is the rightmost LED).
data PlayerLightsConfig =
  PlayerLightsConfig
    { led0 :: LightMode
    , led1 :: LightMode
    , led2 :: LightMode
    , led3 :: LightMode
    }
  deriving (Eq, Read, Show)

-- | Each player light LED can be individually turned on, turned off or used in
-- a pulsating manner (i.e., flashing).
data LightMode
  = LightOn
  | LightOff
  | Flashing
  deriving (Eq, Read, Show)

-- | A convenient player lights configuration where all LEDs are turned off.
noPlayerLights :: PlayerLightsConfig
noPlayerLights =
  PlayerLightsConfig
    { led0 = LightOff
    , led1 = LightOff
    , led2 = LightOff
    , led3 = LightOff
    }

-- | A convenient player lights configuration indicating player one (i.e., 'led0' is set).
playerOne :: PlayerLightsConfig
playerOne = noPlayerLights { led0 = LightOn }

-- | A convenient player lights configuration indicating player two (i.e., 'led1' is set).
playerTwo :: PlayerLightsConfig
playerTwo = noPlayerLights { led1 = LightOn }

-- | A convenient player lights configuration indicating player three (i.e., 'led2' is set).
playerThree :: PlayerLightsConfig
playerThree = noPlayerLights { led2 = LightOn }

-- | A convenient player lights configuration indicating player four (i.e., 'led3' is set).
playerFour :: PlayerLightsConfig
playerFour = noPlayerLights { led3 = LightOn }

-- | A convenient player lights configuration where all LEDs are flashing.
flashAll :: PlayerLightsConfig
flashAll =
  PlayerLightsConfig
    { led0 = Flashing
    , led1 = Flashing
    , led2 = Flashing
    , led3 = Flashing
    }

-- | Sets the player lights of a Nintendo Switch controller.
--
-- Note: After sending a command like this to a controller, it is highly advised
-- to check its corresponding 'Device.Nintendo.Switch.CommandReply'
-- ('Device.Nintendo.Switch.SetPlayerLights', to be exact) or at least call
-- 'Device.Nintendo.Switch.getInput' once before sending another command to
-- that controller. The function 'Device.Nintendo.Switch.withCommandReply' is a
-- convenient way to wait for a specific command reply from the controller.
setPlayerLights :: HasPlayerLights t => PlayerLightsConfig -> Controller t -> IO ()
setPlayerLights config controller =
  sendSubcommand controller 0x01 0x30 $
    [ setBit led0 0x01
    . setBit led1 0x02
    . setBit led2 0x04
    . setBit led3 0x08
    $ 0x00
    ]
  where
    setBit f position =
      case f config of
        LightOn  -> (position .|.)
        LightOff -> id
        Flashing -> (shiftL position 4 .|.)