module Fit.Internal.Parse (
readFitRaw,
parseFit,
parseHeader,
parseMessages,
parseMessage,
parseMessageDef,
parseFieldDef,
parseDataMessage,
parseField,
parseValue,
parseArray,
parseSeq,
parseString,
parseCTDataMessage,
mkHeader
) where
import Fit.Internal.Architecture
import Fit.Internal.FitFile
import Fit.Internal.FitParser
import Control.Applicative ((<$>), (<*>), (<*))
import Control.Monad (replicateM)
import Control.Monad.Trans (lift)
import Data.Attoparsec.ByteString (Parser)
import qualified Data.Attoparsec.ByteString as A (parseOnly, string, anyWord8, takeTill)
import qualified Data.Attoparsec.Combinator as A (count, many')
import Data.Bits (testBit, shiftR, (.&.))
import Data.ByteString (ByteString)
import qualified Data.ByteString as B (init)
import Data.Sequence (Seq)
import qualified Data.Sequence as S (fromList)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Word (Word8)
readFitRaw :: ByteString -> Either String Fit
readFitRaw bs = let noCrc = B.init (B.init bs)
in A.parseOnly parseFit noCrc
parseFit :: Parser Fit
parseFit = runFitParser $ Fit <$> parseHeader <*> parseMessages
parseHeader :: FitParser FitHeader
parseHeader = withArchitecture ArchLittle $ do
headerSize <- word8
protocolVersion <- word8
profileVersion <- archWord16
dataSize <- archWord32
marker <- lift $ A.string ".FIT"
crc <- if headerSize == 14
then Just <$> archWord16
else return Nothing
return $ FH headerSize protocolVersion profileVersion dataSize marker crc
parseMessages :: FitParser [Message]
parseMessages = A.many' parseMessage
parseMessage :: FitParser Message
parseMessage = do
headerByte <- lift A.anyWord8
let header = mkHeader headerByte
case header of
DefHeader t -> do
msgDef <- parseMessageDef t
addMessageDef msgDef
return (DefM msgDef)
DataHeader t -> lookupMessageDef t >>= parseDataMessage
CTDataHeader t o -> lookupMessageDef t >>= parseCTDataMessage o
parseMessageDef :: LocalMessageType -> FitParser MessageDefinition
parseMessageDef lmt = do
_ <- word8
arch <- word8 >>= \case
0 -> return ArchLittle
1 -> return ArchBig
_ -> error "Architecture neither 0 nor 1"
globalNum <- fromIntegral <$> withArchitecture arch archInt16
numFields <- fromIntegral <$> word8
fieldDefs <- replicateM numFields (parseFieldDef)
return $ MessageDef lmt globalNum arch fieldDefs
parseFieldDef :: FitParser FieldDef
parseFieldDef = FieldDef <$> num <*> size <*> baseType
where num = fromIntegral <$> word8
size = fromIntegral <$> word8
baseType = word8 >>= return . \case
0x00 -> FitEnum
0x01 -> FitSInt8
0x02 -> FitUInt8
0x83 -> FitSInt16
0x84 -> FitUInt16
0x85 -> FitSInt32
0x86 -> FitUInt32
0x07 -> FitString
0x88 -> FitFloat32
0x89 -> FitFloat64
0x0A -> FitUInt8Z
0x8B -> FitUInt16Z
0x8C -> FitUInt32Z
0x0D -> FitByte
_ -> error "Invalid base type field"
parseDataMessage :: MessageDefinition -> FitParser Message
parseDataMessage (MessageDef lmt gmt arch fieldDefs) = withArchitecture arch $ do
fields <- mapM parseField fieldDefs
return (DataM lmt gmt fields)
parseField :: FieldDef -> FitParser Field
parseField (FieldDef num size bt) = do
let numValues = size `div` (btSize bt)
field <- if numValues == 1 || (bt == FitString)
then SingletonField num <$> parseValue bt
else ArrayField num <$> parseArray num bt
case field of
TimestampField t -> storeTimestamp (Timestamp t) >> return field
_ -> return field
parseValue :: BaseType -> FitParser Value
parseValue bt =
case bt of
FitEnum -> EnumValue <$> word8
FitSInt8 -> SInt8Value <$> int8
FitUInt8 -> UInt8Value <$> word8
FitSInt16 -> SInt16Value <$> archInt16
FitUInt16 -> UInt16Value <$> archWord16
FitSInt32 -> SInt32Value <$> archInt32
FitUInt32 -> UInt32Value <$> archWord32
FitString -> StringValue <$> lift parseString
FitFloat32 -> Float32Value <$> archFloat32
FitFloat64 -> Float64Value <$> archFloat64
FitUInt8Z -> UInt8ZValue <$> word8
FitUInt16Z -> UInt16ZValue <$> archWord16
FitUInt32Z -> UInt32ZValue <$> archWord32
FitByte -> ByteValue <$> word8
parseArray :: Int
-> BaseType
-> FitParser Array
parseArray n bt = let seqOf = parseSeq n
in case bt of
FitEnum -> EnumArray <$> seqOf word8
FitSInt8 -> SInt8Array <$> seqOf int8
FitUInt8 -> UInt8Array <$> seqOf word8
FitSInt16 -> SInt16Array <$> seqOf archInt16
FitUInt16 -> UInt16Array <$> seqOf archWord16
FitSInt32 -> SInt32Array <$> seqOf archInt32
FitUInt32 -> UInt32Array <$> seqOf archWord32
FitString -> error "String arrays not supported -- how was this line reached?"
FitFloat32 -> Float32Array <$> seqOf (fmap fromIntegral archInt32)
FitFloat64 -> Float64Array <$> seqOf (fmap fromIntegral archInt64)
FitUInt8Z -> UInt8ZArray <$> seqOf word8
FitUInt16Z -> UInt16ZArray <$> seqOf archWord16
FitUInt32Z -> UInt32ZArray <$> seqOf archWord32
FitByte -> ByteArray <$> seqOf word8
parseSeq :: Int -> FitParser a -> FitParser (Seq a)
parseSeq n p = S.fromList <$> A.count n p
parseString :: Parser Text
parseString = decodeUtf8 <$> A.takeTill (== 0) <* A.anyWord8
parseCTDataMessage :: TimeOffset -> MessageDefinition -> FitParser Message
parseCTDataMessage offset (MessageDef lmt gmt arch fieldDefs) = withArchitecture arch $ do
fields <- mapM parseField fieldDefs
newTimestamp <- updateTimestamp offset
let timestampField = TimestampField (unTimestamp newTimestamp)
return $ DataM lmt gmt (timestampField : fields)
mkHeader :: Word8 -> MessageHeader
mkHeader byte =
if isNormalHeader
then if isDefMessage
then defHeader
else dataHeader
else ctDataHeader
where
isNormalHeader = not (testBit byte 7)
isDefMessage = testBit byte 6
defHeader = DefHeader normalLmt
dataHeader = DataHeader normalLmt
ctDataHeader = CTDataHeader ctLmt ctOffset
normalLmt = mkLocalMessageType byte
ctLmt = mkLocalMessageType $ (byte `shiftR` 5) .&. 0x3
ctOffset = mkTimeOffset byte
pattern TimestampField t = SingletonField 253 (UInt32Value t)