-- | The Messages API abtracts over the structure of a FIT file slightly and presents -- the FIT file as just the sequence of data messages in the file. The Messages API -- also abstracts over the various FIT base types (for example, signed/unsigned integers -- of different sizes) to give a simpler set of types to work with. -- -- If you need to know about the very specifics of the FIT file structure, use the Raw API -- instead. However, for pulling information out of a FIT file this API is much more -- convenient. module Fit.Messages ( readMessages, readFileMessages, parseMessages, Messages(..), Message(..), Field(..), Value(..), SingletonValue(..), ArrayValue(..) ) where import qualified Fit.Internal.FitFile as Raw import qualified Fit.Internal.Parse as Raw import Control.Applicative ((<$>)) import Data.Attoparsec.ByteString (Parser) import Data.ByteString (ByteString) import qualified Data.ByteString as B (readFile, pack) import qualified Data.Foldable as F (toList) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as Map (empty, insert) import Data.Maybe (catMaybes) import Data.Sequence (Seq) import qualified Data.Sequence as S (fromList) import Data.Text (Text) import Data.Word (Word8) -- | The collection of data messages from the FIT file. newtype Messages = Messages { _messages :: Seq Message } deriving (Show) -- | A FIT data message data Message = Message { _mNumber :: !Int, -- ^ The global message number, as found in the FIT profile _mFields :: IntMap Field -- ^ The fields in the message, mapped from field number to @Field@ } deriving (Show) -- | A single field in a FIT data message data Field = Field { _fNumber :: !Int, -- ^ The field number, as found in the FIT profile _fValue :: Value } deriving (Show) -- | FIT values can either contain a single piece of data or an array. FIT arrays are homogenous data Value = Singleton SingletonValue | Array ArrayValue deriving (Show) -- | A singleton value. In the Messages API we abstract over the specific FIT base type of the field. For example, the FIT types uint8, sint8, uint16, etc. are all presented as an @IntValue@. FIT strings (ie. character arrays) are presented as singleton @TextValue@s. If you need to know the specific base type of a field you can use the Raw API. data SingletonValue = IntValue !Int | RealValue !Double | ByteValue !Word8 | TextValue Text deriving (Show) -- | Array values. Like singleton values these ignore the specific FIT base type to present a simpler interface. Byte arrays are presented as strict @ByteString@s. There are no character arrays, since the singleton @TextValue@ handles that case. data ArrayValue = IntArray (Seq Int) | RealArray (Seq Double) | ByteArray ByteString deriving (Show) -- | Parse a strict @ByteString@ containing the FIT data into its @Messages@ readMessages :: ByteString -> Either String Messages readMessages bs = toMessages <$> Raw.readFitRaw bs -- | Parse the given FIT file into its @Messages@ readFileMessages :: FilePath -> IO (Either String Messages) readFileMessages fp = B.readFile fp >>= return . readMessages -- | An Attoparsec parser for @Messages@ parseMessages :: Parser Messages parseMessages = fmap toMessages Raw.parseFit toMessages :: Raw.Fit -> Messages toMessages rFit = Messages . S.fromList . catMaybes $ fmap toMessage (Raw.fMessages rFit) toMessage :: Raw.Message -> Maybe Message toMessage (Raw.DefM _) = Nothing toMessage (Raw.DataM _ gmt fields) = Just $ Message gmt (foldr go Map.empty fields) where go f@(Raw.SingletonField num _) fieldMap = Map.insert num (toField f) fieldMap go f@(Raw.ArrayField num _) fieldMap = Map.insert num (toField f) fieldMap toField :: Raw.Field -> Field toField (Raw.SingletonField num value) = Field num . Singleton $ (fromSingletonValue value) toField (Raw.ArrayField num array) = Field num . Array $ (fromArray array) fromSingletonValue :: Raw.Value -> SingletonValue fromSingletonValue v = case v of Raw.EnumValue i -> IntValue (fromIntegral i) Raw.SInt8Value i -> IntValue (fromIntegral i) Raw.UInt8Value i -> IntValue (fromIntegral i) Raw.SInt16Value i -> IntValue (fromIntegral i) Raw.UInt16Value i -> IntValue (fromIntegral i) Raw.SInt32Value i -> IntValue (fromIntegral i) Raw.UInt32Value i -> IntValue (fromIntegral i) Raw.StringValue t -> TextValue t Raw.Float32Value f -> RealValue (fromRational (toRational f)) Raw.Float64Value f -> RealValue f Raw.UInt8ZValue i -> IntValue (fromIntegral i) Raw.UInt16ZValue i -> IntValue (fromIntegral i) Raw.UInt32ZValue i -> IntValue (fromIntegral i) Raw.ByteValue b -> ByteValue b fromArray :: Raw.Array -> ArrayValue fromArray a = case a of Raw.EnumArray xs -> intArray xs Raw.SInt8Array xs -> intArray xs Raw.UInt8Array xs -> intArray xs Raw.SInt16Array xs -> intArray xs Raw.UInt16Array xs -> intArray xs Raw.SInt32Array xs -> intArray xs Raw.UInt32Array xs -> intArray xs Raw.Float32Array xs -> realArray xs Raw.Float64Array xs -> realArray xs Raw.UInt8ZArray xs -> intArray xs Raw.UInt16ZArray xs -> intArray xs Raw.UInt32ZArray xs -> intArray xs Raw.ByteArray bs -> ByteArray . B.pack $ F.toList bs where intArray is = IntArray $ fmap fromIntegral is realArray rs = RealArray $ fmap (fromRational . toRational) rs