packages feed

gpmf-0.1.1.2: src/GoPro/GPMF.hs

{-|
Module: GoPro.GPMF
Description: Parser for GoPro GPMF telemetry data.
Copyright: (c) Dustin Sallings, 2020
License: BSD3
Maintanier: dustin@spy.net
Stability: experimental

A low-level parser for <https://github.com/gopro/gpmf-parser GPMF> telemetry data.
-}

{-# LANGUAGE TupleSections #-}
{-# LANGUAGE DeriveGeneric #-}

module GoPro.GPMF (parseGPMF, Value(..), FourCC(..)) where

import           Control.Monad                    (replicateM)
import           Control.Monad.State              (StateT, evalStateT, get, lift, put)
import           Data.Attoparsec.Binary           (anyWord16be, anyWord32be, anyWord64be)
import qualified Data.Attoparsec.ByteString       as A
import qualified Data.Attoparsec.ByteString.Char8 as AC
import           Data.Binary.Get                  (getInt32be, runGet)
import           Data.Binary.IEEE754              (getFloat32be)
import qualified Data.ByteString                  as BS
import qualified Data.ByteString.Lazy             as BL
import           Data.Int                         (Int16, Int32, Int64, Int8)
import           Data.String                      (IsString (..))
import           Data.Time.Clock                  (UTCTime)
import           Data.Time.Format                 (defaultTimeLocale, parseTimeM)
import           Data.Word                        (Word16, Word32, Word64, Word8)
import GHC.Generics (Generic)

{-
Type Char	Definition	typedef	Comment
b	single byte signed integer	int8_t	-128 to 127
B	single byte unsigned integer	uint8_t	0 to 255
c	single byte 'c' style ASCII character string	char	Optionally NULL terminated - size/repeat sets the length
d	64-bit double precision (IEEE 754)	double
f	32-bit float (IEEE 754)	float
F	32-bit four character key -- FourCC	char fourcc[4]
G	128-bit ID (like UUID)	uint8_t guid[16]
j	64-bit signed unsigned number	int64_t
J	64-bit unsigned unsigned number	uint64_t
l	32-bit signed integer	int32_t
L	32-bit unsigned integer	uint32_t
q	32-bit Q Number Q15.16	uint32_t	16-bit integer (A) with 16-bit fixed point (B) for A.B value (range -32768.0 to 32767.99998)
Q	64-bit Q Number Q31.32	uint64_t	32-bit integer (A) with 32-bit fixed point (B) for A.B value.
s	16-bit signed integer	int16_t	-32768 to 32768
S	16-bit unsigned integer	uint16_t	0 to 65536
U	UTC Date and Time string	char utcdate[16]	Date + UTC Time format yymmddhhmmss.sss - (years 20xx covered)
?	data structure is complex	TYPE	Structure is defined with a preceding TYPE
null	Nested metadata	uint32_t	The data within is GPMF structured KLV data
-}

newtype FourCC = FourCC (Char, Char, Char, Char) deriving (Show, Eq, Generic)

instance IsString FourCC where
  fromString [a,b,c,d] = FourCC (a,b,c,d)
  fromString _         = error "invalid FourCC"

data Value = GInt8 [Int8]
    | GUint8 [Word8]
    | GString String
    | GDouble Double
    | GFloat [Float]
    | GFourCC FourCC
    | GUUID [Word8]
    | GInt64 [Int64]
    | GUint64 [Word64]
    | GInt32 [Int32]
    | GUint32 [Word32]
    | GQ32 [Word32]
    | GQ64 [Word64]
    | GInt16 [Int16]
    | GUint16 [Word16]
    | GTimestamp UTCTime
    | GComplex String [Value]
    | GNested (FourCC, [Value])
    | GUnknown (Char, Int, Int, [[Word8]])
    deriving (Show, Generic)

type Parser = StateT String A.Parser

anyInt8 :: A.Parser Int8
anyInt8 = fromIntegral <$> A.anyWord8

-- | Parse GPMF data from a telemetry stream.  A successful return
-- value contains a list of FourCC tagged value lists.
--
-- Note that the input is the telemetry stream itself, not the
-- container that contains it.
parseGPMF :: BS.ByteString -> Either String [(FourCC, [Value])]
parseGPMF = A.parseOnly (evalStateT (A.many1 parseNested) "")

parseNested :: Parser (FourCC, [Value])
parseNested = do
  fourcc <- lift parseFourCC
  t <- lift AC.anyChar
  ss <- fromIntegral <$> lift A.anyWord8
  rpt <- fromIntegral <$> lift anyWord16be
  let padding = (4 - (fromIntegral ss * rpt) `mod` 4) `mod` 4

  stuffs <- parseValue t ss rpt

  case (fourcc, stuffs) of
    ("TYPE", [GString x]) -> put x
    _                     -> pure ()

  _ <- lift $ replicateM padding A.anyWord8
  pure (fourcc, stuffs)

parseString :: Int -> A.Parser Value
parseString l = GString . reverse  . dropWhile (== '\0') . reverse <$> replicateM l AC.anyChar

parseFloat :: A.Parser Float
parseFloat = runGet getFloat32be . BL.fromStrict <$> A.take 4

parseInt32 :: A.Parser Int32
parseInt32 = runGet getInt32be . BL.fromStrict <$> A.take 4

replicatedParser :: Int -> Int -> Int -> A.Parser a -> ([a] -> Value) -> Parser [Value]
replicatedParser 0 l rpt _ _ = lift $ replicateM (l*rpt) A.anyWord8 >> pure []
replicatedParser one l rpt p cons =
  fmap cons <$> lift (replicateM rpt (replicateM (l `div` one) p))

parseTimestamp :: A.Parser UTCTime
parseTimestamp = parseTimeM False defaultTimeLocale "%y%m%d%H%M%S%Q" =<< replicateM 16 AC.anyChar

singleParser :: Char -> (Int, A.Parser Value)
singleParser 'F' = (4, GFourCC <$> parseFourCC)
singleParser 'f' = (4, GFloat . (:[]) <$> parseFloat)
singleParser 'L' = (4, GUint32 . (:[]) <$> anyWord32be)
singleParser 'l' = (4, GInt32 . (:[]) <$> parseInt32)
singleParser 'B' = (1, GUint8 . (:[]) <$> A.anyWord8)
singleParser 'b' = (1, GInt8 . (:[]) <$> anyInt8)
singleParser 'S' = (1, GUint16 . (:[]) <$> anyWord16be)
singleParser 's' = (1, GUint16 . (:[]) . fromIntegral <$> anyWord16be)
singleParser x   = error ("unsupported parser: " <> show x)

parseComplex :: Int -> Int -> Parser [Value]
parseComplex l rpt = do
  fmt <- get
  let sz = foldr (\x o -> (fst . singleParser) x + o) 0 fmt
  let parsers = traverse (snd . singleParser) fmt
  replicatedParser sz l rpt parsers (GComplex fmt . mconcat)

parseValue :: Char -> Int -> Int -> Parser [Value]
parseValue '\0' l rpt = do
  inp <- lift $ A.take (l * rpt)
  t <- get
  fmap GNested <$> either fail pure (A.parseOnly (evalStateT (A.many1 parseNested) t) inp)
parseValue 'F' 4 rpt = lift $ replicateM rpt (GFourCC <$> parseFourCC)
parseValue 'L' l rpt = replicatedParser 4 l rpt anyWord32be GUint32
parseValue 'l' l rpt = replicatedParser 4 l rpt (fromIntegral <$> anyWord32be) GInt32
parseValue 'c' l rpt = (:[]) <$> (lift . parseString $ (l * rpt))
parseValue 's' l rpt = replicatedParser 2 l rpt (fromIntegral <$> anyWord16be) GInt16
parseValue 'S' l rpt = replicatedParser 2 l rpt anyWord16be GUint16
parseValue 'J' l rpt = replicatedParser 8 l rpt anyWord64be GUint64
parseValue 'f' l rpt = replicatedParser 4 l rpt parseFloat GFloat
parseValue 'b' l rpt = lift $ replicateM rpt (GInt8 <$> replicateM l anyInt8)
parseValue 'B' l rpt = lift $ replicateM rpt (GUint8 <$> replicateM l A.anyWord8)
parseValue 'U' 16 1 = (:[]) . GTimestamp <$> lift parseTimestamp
parseValue '?' l rpt = parseComplex l rpt
parseValue x l rpt = do
  u <- lift $ replicateM rpt (replicateM l A.anyWord8)
  pure [GUnknown (x, l, rpt, u)]

parseFourCC :: A.Parser FourCC
parseFourCC = do
  a <- AC.anyChar
  b <- AC.anyChar
  c <- AC.anyChar
  d <- AC.anyChar
  pure $ FourCC (a,b,c,d)