{-# LANGUAGE LambdaCase #-} module Fit.Internal.FitParser ( -- * FitParser FitParser, runFitParser, FpState(..), Definitions(..), addMessageDef, lookupMessageDef, withArchitecture, storeTimestamp, updateTimestamp, -- * Architecture-independent parsers word8, int8, -- * Architecture-dependent parsers -- $archparsers archWord16, archWord32, archWord64, archInt16, archInt32, archInt64, archFloat32, archFloat64 ) where import Fit.Internal.Architecture import Fit.Internal.FitFile import qualified Fit.Internal.Numbers as N import Control.Applicative ((<$>), (<*), (*>)) import Control.Monad.State.Class (get, modify) import Control.Monad.State.Strict (StateT, evalStateT) import Control.Monad.Trans (lift) import Data.Attoparsec.ByteString (Parser) import qualified Data.Attoparsec.ByteString as A (anyWord8) import Data.Bits ((.&.)) import Data.Int (Int8, Int16, Int32, Int64) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IntMap (insert, lookup, empty) import Data.Word (Word8, Word16, Word32, Word64) type FitParser a = StateT FpState Parser a -- | Turn a 'FitParser' into a plain attoparsec 'Parser'. This doesn't require any -- configuration as the initial state for a FIT parse is always the same. runFitParser :: FitParser a -> Parser a runFitParser = flip evalStateT (FpState ArchLittle defEmpty Nothing) -- | Little-endian interpretation is used by default by 'FitParser'. -- Use this function to set the endianness to use for the scope of a particular action. -- After the action is finished the previous endianness is restored. withArchitecture :: Arch -> FitParser a -> FitParser a withArchitecture arch action = use fpArch >>= \old -> setArch arch *> action <* setArch old setArch :: Arch -> FitParser () setArch = assign fpArch -- | Register a 'MessageDefinition' with the parser, so it can decode -- subsequent data messages using the definition addMessageDef :: MessageDefinition -> FitParser () addMessageDef def = fpMessageDefs %= (defAdd def) -- | Look up the 'MessageDefinition' for the given message type. -- It is an error to look up a message type that has no registered definition, -- since it is impossible to decode a data message with no definition lookupMessageDef :: LocalMessageType -> FitParser MessageDefinition lookupMessageDef lmt = do msgDefs <- use fpMessageDefs case defLookup lmt msgDefs of Just def -> return def Nothing -> error $ "No definition for local type " ++ (show lmt) -- | Store the given 'Timestamp' as the most recent. Is used to store timestamps -- from non-compressed timestamp messages. For compressed-timestamp messages use -- 'updateTimestamp' instead. storeTimestamp :: Timestamp -> FitParser () storeTimestamp t = fpLastTimestamp .= Just t -- | Use the given 'TimeOffset' and the previous 'Timestamp' to compute a new -- Timestamp. The new 'Timestamp' is stored as most recent and is returned. -- -- This function fails if there is no previously-stored 'Timestamp'. This -- condition should never come up when parsing a valid FIT file. updateTimestamp :: TimeOffset -> FitParser Timestamp updateTimestamp offset = do previous <- use fpLastTimestamp let new = addOffset offset previous fpLastTimestamp .= Just new return new where addOffset _ Nothing = error "No base timestamp to update" addOffset (TO off) (Just (Timestamp previous)) = let off' = fromIntegral off low5Prev = previous .&. 0x1F high27Prev = previous .&. 0xFFFFFFE0 rollover = off' < low5Prev in if rollover then Timestamp $ high27Prev + 0x20 + off' else Timestamp $ high27Prev + off' -- | The necessary state for parsing FIT files data FpState = FpState { _fpArch :: !Arch, -- ^ The active endian-ness _fpMessageDefs :: Definitions, -- ^ The set of active message definitions _fpLastTimestamp :: !(Maybe Timestamp) -- ^ The most recently stored timestamp } -- | The definitions are stored as a map on the local message type number. When a definition -- is parsed with a previously-used local message type, the previous definition is -- overwritten. newtype Definitions = Defs { unDefs :: IntMap MessageDefinition } {- Lenses for FpState -} fpArch :: Functor f => Lens f FpState Arch fpArch f (FpState arch defs ts) = (\arch' -> FpState arch' defs ts) <$> (f arch) fpMessageDefs :: Functor f => Lens f FpState Definitions fpMessageDefs f (FpState arch defs ts) = (\defs' -> FpState arch defs' ts) <$> (f defs) fpLastTimestamp :: Functor f => Lens f FpState (Maybe Timestamp) fpLastTimestamp f (FpState arch defs ts) = (\ts' -> FpState arch defs ts') <$> (f ts) defAdd :: MessageDefinition -> Definitions -> Definitions defAdd md = Defs . (IntMap.insert lmt md) . unDefs where lmt = unLocalMessageType . defLocalType $ md defLookup :: LocalMessageType -> Definitions -> Maybe MessageDefinition defLookup (LMT lmt) (Defs defs) = IntMap.lookup (fromIntegral lmt) defs defEmpty :: Definitions defEmpty = Defs (IntMap.empty) word8 :: FitParser Word8 word8 = lift A.anyWord8 int8 :: FitParser Int8 int8 = fromIntegral <$> word8 -- $archparsers -- The following parsers are all sensitive to the active endianness. For example, -- 'archWord16' will use a little-endian or big-endian interpretation according -- to the architecture for the 'MessageDefinition' for the current message. -- Internally, these parsers use the endian-specific parsers from "Fit.Internal.Numbers". -- | Parse a Word16 using the active endianness archWord16 :: FitParser Word16 archWord16 = withArch N.word16le N.word16be -- | Parse a Word32 using the active endianness archWord32 :: FitParser Word32 archWord32 = withArch N.word32le N.word32be -- | Parse a Word64 using the active endianness archWord64 :: FitParser Word64 archWord64 = withArch N.word64le N.word64be -- | Parse an Int16 using the active endianness archInt16 :: FitParser Int16 archInt16 = withArch N.int16le N.int16be -- | Parse an Int32 using the active endiannessa archInt32 :: FitParser Int32 archInt32 = withArch N.int32le N.int32be -- | Parse an Int64 using the active endianness archInt64 :: FitParser Int64 archInt64 = withArch N.int64le N.int64be -- | Parse a Float using the active endianness archFloat32 :: FitParser Float archFloat32 = withArch N.float32le N.float32be -- | Parse a Double using the active endianness archFloat64 :: FitParser Double archFloat64 = withArch N.float64le N.float64be -- | Perform an architecture-sensitive operation with separate -- actions for little- and big-endian parsing withArch :: LittleEndian (Parser a) -> BigEndian (Parser a) -> FitParser a withArch little big = use fpArch >>= \case ArchLittle -> lift (unArch little) ArchBig -> lift (unArch big) {- Quick lens implementation for handling state in FitParser -} view :: Getter s a -> s -> a view l x = getConst $ l Const x over :: Setter s a -> (a -> a) -> s -> s over l f x = runIdentity $ l (Identity . f) x set :: Setter s a -> a -> s -> s set l x = over l (const x) use :: (Monad m, Functor m) => Getter s a -> StateT s m a use l = fmap (view l) get assign :: (Monad m, Functor m) => Setter s a -> a -> StateT s m () assign l x = modify (set l x) (.=) :: (Monad m, Functor m) => Setter s a -> a -> StateT s m () l .= x = assign l x (%=) :: (Monad m, Functor m) => Setter s a -> (a -> a) -> StateT s m () l %= f = modify (over l f) type Lens f s a = (a -> f a) -> s -> f s type Getter s a = Lens (Const a) s a type Setter s a = Lens Identity s a newtype Identity a = Identity { runIdentity :: a } instance Functor Identity where fmap f (Identity a) = Identity (f a) newtype Const a b = Const { getConst :: a } instance Functor (Const a) where fmap _ (Const x) = (Const x)