packages feed

fxpak-0.1.0: src/System/Hardware/FXPak.hs

{-# language DataKinds         #-}
{-# language GADTs             #-}
{-# language KindSignatures    #-}
{-# language OverloadedStrings #-}
{-# language ViewPatterns      #-}
{- |
Module      :  System.Hardware.FXPak
Copyright   :  (c) Christina Wuest 2021
License     :  BSD-style

Maintainer  :  tina@wuest.me
Stability   :  experimental
Portability :  non-portable

Interface to working with the FXPak/FXPak Pro flash cart devices for the
SNES/Super Famicom
-}

module System.Hardware.FXPak ( FXPak
                             , Packet, Opcode(..), Context(..), Arguments(..)
                             , FI.AddressGet(..), FI.AddressSet(..)
                             , FI.Flag(..), Flags
                             , open, packet, send
                             ) where

import Prelude

import qualified System.Hardware.Serialport as Serial
import qualified Data.ByteString.Char8 as BS

import Control.Monad.IO.Class ( liftIO )
import Data.Bits ( (.&.), shiftR )
import Data.Char ( chr, ord )

import qualified System.Hardware.FXPak.Internal as FI

-- | An FXPak device (which is exposed as a serial device)
type FXPak = Serial.SerialPort

-- | A packet representing a single complete to send to the FXPak
type Packet = FI.Packet

-- | Represents an address to fetch memory from and a length to read
type AddressGet = FI.AddressGet

-- | Represents a value and the address to which it should be written
type AddressSet = FI.AddressSet

-- | The context in which a packet's command operates
data Context c where
    -- | In the File mode, files can be managed directly, and booted
    File   :: Context (FI.Context' 'FI.File)
    -- | In the SNES context, bytes can be written to and read from memory, and
    -- all system commands (e.g. reset, powercycle, reset to menu, system info,
    -- and stream data) are passed through
    SNES   :: Context (FI.Context' 'FI.SNES)
    MSU    :: Context (FI.Context' 'FI.MSU)
    Config :: Context (FI.Context' 'FI.Config)

-- | The Opcode representing the command to perform
data Opcode o where
    -- | Depending on context, either return the contents of a given file, or
    -- read memory
    Get        :: Opcode (FI.Opcode' 'FI.Get)
    -- | Depending on context, either allow the upload of a file, or write bytes
    -- to memory
    Put        :: Opcode (FI.Opcode' 'FI.Put)
    -- | Retrieve from 1-4 regions of memory
    VGet       :: Opcode (FI.Opcode' 'FI.VGet)
    -- | Write from 1-4 bytes to memory
    VPut       :: Opcode (FI.Opcode' 'FI.VPut)
    -- | List files in a given directory
    List       :: Opcode (FI.Opcode' 'FI.List)
    -- | Make a given directory on the FXPak's filesystem
    Mkdir      :: Opcode (FI.Opcode' 'FI.Mkdir)
    -- | Delete a given path on the FXPak's filesystem
    Delete     :: Opcode (FI.Opcode' 'FI.Delete)
    -- | Move a given path on the FXPak's filesystem to a new location
    Move       :: Opcode (FI.Opcode' 'FI.Move)
    -- | Reset the SNES/SFC
    Reset      :: Opcode (FI.Opcode' 'FI.Reset)
    -- | Boot a given file
    Boot       :: Opcode (FI.Opcode' 'FI.Boot)
    -- | Reset the SNES/SFC
    PowerCycle :: Opcode (FI.Opcode' 'FI.PowerCycle)
    -- | Return information about the running FXPak
    Info       :: Opcode (FI.Opcode' 'FI.Info)
    -- | Reset the SNES/SFC, returning to the FXPak's main menu
    MenuReset  :: Opcode (FI.Opcode' 'FI.MenuReset)
    Stream     :: Opcode (FI.Opcode' 'FI.Stream)
    Time       :: Opcode (FI.Opcode' 'FI.Time)
    -- | Indicates a response packet
    Response   :: Opcode (FI.Opcode' 'FI.Response)

-- | Arguments indicating the desired action taken by a given operation
data Arguments a where
    -- | No arguments - valid only with the Reset, MenuReset, Info, Stream, and
    -- PowerCycle opcodes
    None         :: Arguments (FI.Arguments' 'FI.None)
    -- | Path to a given object - valid for Get, List, Mkdir, Delete, and Boot
    -- in the File context
    Path         :: FilePath -> Arguments (FI.Arguments' ('FI.Path (a :: FilePath)))
    -- | Path with an accompanying ByteString - valid only for Put in the File
    -- context
    PathContents :: FilePath -> BS.ByteString -> Arguments (FI.Arguments' ('FI.PathContents (a :: FilePath) (b :: BS.ByteString)))
    -- | Source and destination paths - valid only for Move in the File context
    PathRename   :: FilePath -> FilePath -> Arguments (FI.Arguments' ('FI.PathRename (a :: FilePath) a))
    -- | An address and length of data to be read for non-File context Get and
    -- VGet opcodes
    GetBytes     :: AddressGet -> Arguments (FI.Arguments' ('FI.GetBytes (a :: AddressGet)))
    -- | Two address/length pairs to be read for non-File context VGet
    GetBytes2    :: AddressGet -> AddressGet -> Arguments (FI.Arguments' ('FI.GetBytes2 (a :: (AddressGet, AddressGet))))
    -- | Three address/length pairs to be read for non-File context VGet
    GetBytes3    :: AddressGet -> AddressGet -> AddressGet -> Arguments (FI.Arguments' ('FI.GetBytes3 (a :: (AddressGet, AddressGet, AddressGet))))
    -- | Four address/length pairs to be read for non-File context VGet
    GetBytes4    :: AddressGet -> AddressGet -> AddressGet -> AddressGet -> Arguments (FI.Arguments' ('FI.GetBytes4 (a :: (AddressGet, AddressGet, AddressGet, AddressGet))))
    -- | A target address and byte to be written for non-File context Put and
    -- VPut opcodes
    SetByte      :: AddressSet -> Arguments (FI.Arguments' ('FI.SetByte (a :: AddressSet)))
    -- | Two address/data pairs to be written for non-File context VPut
    SetByte2     :: AddressSet -> AddressSet -> Arguments (FI.Arguments' ('FI.SetByte2 (a :: (AddressSet, AddressSet))))
    -- | Three address/data pairs to be written for non-File context VPut
    SetByte3     :: AddressSet -> AddressSet -> AddressSet -> Arguments (FI.Arguments' ('FI.SetByte3 (a :: (AddressSet, AddressSet, AddressSet))))
    -- | Four address/data pairs to be written for non-File context VPut
    SetByte4     :: AddressSet -> AddressSet -> AddressSet -> AddressSet -> Arguments (FI.Arguments' ('FI.SetByte4 (a :: (AddressSet, AddressSet, AddressSet, AddressSet))))

-- | List of Flags to be encoded for a given packet
type Flags = FI.Flags

-- | Given a Context (wrapping the internal type-safe Context' construct),
-- produce the internal construct for the purposes of packet construction
context :: Context (FI.Context' c) -> FI.Context' c
context File   = FI.File'
context SNES   = FI.SNES'
context MSU    = FI.MSU'
context Config = FI.Config'

-- | Given an Opcode (wrapping the internal type-safe Opcode' construct),
-- produce the internal construct for the purposes of packet construction
opcode :: Opcode (FI.Opcode' o) -> FI.Opcode' o
opcode Get        = FI.Get'
opcode Put        = FI.Put'
opcode VGet       = FI.VGet'
opcode VPut       = FI.VPut'
opcode List       = FI.List'
opcode Mkdir      = FI.Mkdir'
opcode Delete     = FI.Delete'
opcode Move       = FI.Move'
opcode Reset      = FI.Reset'
opcode Boot       = FI.Boot'
opcode PowerCycle = FI.PowerCycle'
opcode Info       = FI.Info'
opcode MenuReset  = FI.MenuReset'
opcode Stream     = FI.Stream'
opcode Time       = FI.Time'
opcode Response   = FI.Response'

-- | Given an Arguments datum (wrapping the internal type-safe Arguments'
-- construct), -- produce the internal construct for the purposes of packet
-- construction
arguments :: Arguments (FI.Arguments' a) -> FI.Arguments' a
arguments None                = FI.None'
arguments (Path a)            = FI.Path' a
arguments (PathContents a b)  = FI.PathContents' a b
arguments (PathRename a b)    = FI.PathRename' a b
arguments (GetBytes a)        = FI.GetBytes' a
arguments (GetBytes2 a b)     = FI.GetBytes2' a b
arguments (GetBytes3 a b c)   = FI.GetBytes3' a b c
arguments (GetBytes4 a b c d) = FI.GetBytes4' a b c d
arguments (SetByte a)         = FI.SetByte' a
arguments (SetByte2 a b)      = FI.SetByte2' a b
arguments (SetByte3 a b c)    = FI.SetByte3' a b c
arguments (SetByte4 a b c d)  = FI.SetByte4' a b c d

-- | Open a given serial device as an FXPak
open :: FilePath -> IO FXPak
open = flip Serial.openSerial Serial.defaultSerialSettings

-- | Encode a packet to send to an FXPak, preventing encoding of invalid packets
packet :: (FI.ValidPacket c o a ~ 'True) => Context (FI.Context' c) -> Opcode (FI.Opcode' o) -> Flags -> Arguments (FI.Arguments' a) -> Packet
packet (context -> c) (opcode -> o) flags (arguments -> a) = FI.packet c o flags a

-- | Sends a given packet to an FXPak device, returning a ByteString if a
-- response is expected
send :: FXPak -> Packet -> IO (Maybe BS.ByteString)
send dev dat = do
    _ <- Serial.send dev $ pack dat
    let (FI.Packet _ _ flags _) = dat
    if elem FI.NoResponse flags
        then return Nothing
        else do
            resp <- liftIO $ readSerial 512 dev []
            let resp' = BS.unpack resp in
                if (take 5 resp') /= ['U', 'S', 'B', 'A', '\x0F']
                    then return Nothing
                    else fetch dev $ fmap ord $ drop 255 $ resp'

-- | Read a number of bytes from the FXPak designated by a string fro
-- a string read from the serial device
fetch :: FXPak -> [Int] -> IO (Maybe BS.ByteString)
fetch _ []         = return Nothing
fetch _ (0:_)      = return Nothing
fetch dev (size:_) = Just <$> readSerial size dev []

-- | Convert a Packet to a ByteString ready to be sent to the FXPak device,
-- enforcing the appropriate packet size
pack :: Packet -> BS.ByteString
pack (FI.Packet o c f a) =
    let op = chr $ fromEnum o
        ctx = chr $ fromEnum c
        flags = chr $ FI.fromFlags f
    -- "USBA" string is taken from the original usb2snesw application
    in pack' (elem FI.Data64Bytes f) o a ['U', 'S', 'B', 'A', op, ctx, flags]

-- | Select the appropriate size pack function given the opcode and flags given
pack' :: Bool -> FI.Opcode -> FI.Arguments -> [Char] -> BS.ByteString
pack' True  _       = pack64
pack' False FI.VPut = pack64
pack' False FI.VGet = pack64
pack' False _       = pack512

-- | Given a set of GetBytes/SetByte arguments and packet header, generate a 64
-- byte packet
-- Note: 64 byte packets can only be generated for the VGet/VPut opcodes, and
-- therefore any non-GetBytes/SetByte operations are considered undefined in
-- this context
pack64 :: FI.Arguments -> String -> BS.ByteString
pack64 (FI.GetBytes addrGet) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressGet addrGet) ++ (nulls 24)
pack64 (FI.GetBytes2 (addrGet, addrGet2)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressGet addrGet) ++ (fromAddressGet addrGet2) ++ (nulls 16)
pack64 (FI.GetBytes3 (addrGet, addrGet2, addrGet3)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressGet addrGet) ++ (fromAddressGet addrGet2) ++ (fromAddressGet addrGet3) ++ (nulls 8)
pack64 (FI.GetBytes4 (addrGet, addrGet2, addrGet3, addrGet4)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressGet addrGet) ++ (fromAddressGet addrGet2) ++ (fromAddressGet addrGet3) ++ (fromAddressGet addrGet4)
pack64 (FI.SetByte addrSet) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressSet addrSet) ++ (nulls 24)
pack64 (FI.SetByte2 (addrSet, addrSet2)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressSet addrSet) ++ (fromAddressSet addrSet2) ++ (nulls 16)
pack64 (FI.SetByte3 (addrSet, addrSet2, addrSet3)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressSet addrSet) ++ (fromAddressSet addrSet2) ++ (fromAddressSet addrSet3) ++ (nulls 8)
pack64 (FI.SetByte4 (addrSet, addrSet2, addrSet3, addrSet4)) tmp = BS.pack $ tmp ++ (nulls 25) ++ (fromAddressSet addrSet) ++ (fromAddressSet addrSet2) ++ (fromAddressSet addrSet3) ++ (fromAddressSet addrSet4)
pack64 _ _ = undefined

-- | Given a set of arguments and packet header, generate a 512 byte packet
-- Note: VGet/VPut-specific arguments (GetBytes2+/SetByte2+) are considered
-- undefined in this context, as VGet/VPut must use 64-byte packet
pack512 :: FI.Arguments -> String -> BS.ByteString
pack512 FI.None tmp = BS.pack $ tmp ++ (nulls 505)
pack512 (FI.Path p) tmp =
    let p' = take 255 p
    in BS.pack $ tmp ++ (nulls 249) ++ p' ++ (nulls (256 - (length p')))
pack512 (FI.PathRename s d) tmp =
    let s' = take 255 s
        d' = take 248 d
    in BS.pack $ tmp ++ d' ++ (nulls (249 - (length d'))) ++ s' ++ (nulls (256 - (length s')))
pack512 (FI.GetBytes addrGet) tmp = BS.pack $ tmp ++ (nulls 245) ++ (fromAddressGet addrGet) ++ (nulls 252)
pack512 (FI.SetByte addrSet) tmp = BS.pack $ tmp ++ (nulls 245) ++ (fromAddressSet addrSet) ++ (nulls 252)
pack512 _ _ = undefined

-- | Generate an 8 byte string from an AddressGet
fromAddressGet :: AddressGet -> String
fromAddressGet ag =
    let validSize = chr $ (0xFF .&.) $ FI.dataLength ag
        addr = FI.start ag
        addrhi = chr $ 0xFF .&. (shiftR addr 16)
        addrmid = chr $ 0xFF .&. (shiftR addr 8)
        addrlow = chr $ 0xFF .&. addr
    in (nulls 3) ++ [validSize, chr 0x00, addrhi, addrmid, addrlow]

-- | Generate an 8 byte string from an AddressSet
fromAddressSet :: AddressSet -> String
fromAddressSet as =
    let addr = FI.target as
        byte = chr $ (0xFF .&.) $ FI.value as
        addrhi = chr $ 0xFF .&. (shiftR 16 addr)
        addrmid = chr $ 0xFF .&. (shiftR 8 addr)
        addrlow = chr $ 0xFF .&. addr
    in (nulls 3) ++ [byte, chr 0x00, addrhi, addrmid, addrlow]

-- | Convenience function to generate arbitrary length string of NUL bytes
nulls :: Int -> String
nulls x = take x $ fmap chr $ repeat 0x00

-- | Read a given number of bytes from the FXPak
readSerial :: Int -> FXPak -> [BS.ByteString] -> IO BS.ByteString
readSerial size dev bufs =
    let currSize = sum $ fmap BS.length bufs
    in
        if currSize >= size
           then return $ foldl BS.append "" $ reverse bufs
           else do
               dat <- Serial.recv dev $ size - currSize
               readSerial size dev $ dat:bufs