gnss-converters 0.1.11 → 0.1.12
raw patch · 8 files changed
+235/−165 lines, 8 filesdep +exceptionsdep +monad-controldep +mtldep −criteriondep ~base
Dependencies added: exceptions, monad-control, mtl, transformers-base
Dependencies removed: criterion
Dependency ranges changed: base
Files
- bench/Bench.hs +0/−7
- gnss-converters.cabal +9/−15
- main/RTCM32SBP.hs +17/−15
- src/Data/RTCM3/SBP.hs +99/−85
- src/Data/RTCM3/SBP/Types.hs +74/−0
- src/SwiftNav/SBP/RTCM3.hs +6/−6
- test/Test.hs +0/−3
- test/Test/Data/RTCM3/SBP.hs +30/−34
− bench/Bench.hs
@@ -1,7 +0,0 @@-import BasicPrelude-import Criterion.Main--main :: IO ()-main = defaultMain- [- ]
gnss-converters.cabal view
@@ -1,5 +1,5 @@ name: gnss-converters-version: 0.1.11+version: 0.1.12 synopsis: GNSS Converters. description: Haskell bindings for GNSS converters. homepage: http://github.com/swift-nav/gnss-converters@@ -18,16 +18,22 @@ library hs-source-dirs: src exposed-modules: Data.RTCM3.SBP+ , Data.RTCM3.SBP.Types , SwiftNav.SBP.RTCM3 ghc-options: -Wall build-depends: base >= 4.7 && < 5 , basic-prelude , conduit-extra+ , exceptions , extra , lens+ , monad-control+ , mtl+ , resourcet , rtcm , sbp , time+ , transformers-base , unordered-containers default-language: Haskell2010 default-extensions: NoImplicitPrelude@@ -64,32 +70,20 @@ hs-source-dirs: test main-is: Test.hs other-modules: Test.Data.RTCM3.SBP- build-depends: base+ build-depends: HUnit-approx+ , base , basic-prelude , binary-conduit , bytestring , conduit , conduit-extra , gnss-converters- , HUnit-approx , lens , resourcet , sbp , tasty , tasty-hunit , unordered-containers- ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall- default-language: Haskell2010- default-extensions: NoImplicitPrelude--benchmark bench- type: exitcode-stdio-1.0- hs-source-dirs: bench- main-is: Bench.hs- build-depends: base- , basic-prelude- , criterion- , gnss-converters ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall default-language: Haskell2010 default-extensions: NoImplicitPrelude
main/RTCM32SBP.hs view
@@ -8,20 +8,22 @@ -- -- RTCM3 to SBP tool. -import BasicPrelude-import Control.Monad.Trans.Resource-import Data.Conduit-import Data.Conduit.Binary-import qualified Data.Conduit.List as CL-import Data.Conduit.Serialization.Binary-import Data.RTCM3.SBP-import System.IO+import BasicPrelude+import Control.Monad.Trans.Resource+import Data.Conduit+import Data.Conduit.Binary+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary+import Data.RTCM3.SBP+import Data.RTCM3.SBP.Types+import System.IO main :: IO ()-main = runResourceT $- sourceHandle stdin =$=- conduitDecode =$=- CL.mapMaybeM convert =$=- CL.concat =$=- conduitEncode $$- sinkHandle stdout+main = do+ s <- liftIO $ newStore+ runResourceT $ runConvertT s $+ sourceHandle stdin =$=+ conduitDecode =$=+ CL.concatMapM convert =$=+ conduitEncode $$+ sinkHandle stdout
src/Data/RTCM3/SBP.hs view
@@ -12,15 +12,17 @@ module Data.RTCM3.SBP where -import BasicPrelude-import Control.Lens-import Data.Bits-import qualified Data.HashMap.Strict as H-import Data.List.Extra hiding (concat, map)-import Data.Time-import Data.Word-import Data.RTCM3-import SwiftNav.SBP+import BasicPrelude+import Control.Lens+import Data.Bits+import qualified Data.HashMap.Strict as M+import Data.IORef+import Data.List.Extra hiding (concat, map)+import Data.RTCM3+import Data.RTCM3.SBP.Types+import Data.Time+import Data.Word+import SwiftNav.SBP --------------------------------------------------------------------------------@@ -104,17 +106,20 @@ -- | Map L2 codes to SBP GnssSignal codes ---l2codeToSBPSignalCode :: H.HashMap Word8 Word8-l2codeToSBPSignalCode = H.fromList [(0, l2CMSidCode), (1, l2PSidCode)]+l2codeToSBPSignalCode :: HashMap Word8 Word8+l2codeToSBPSignalCode = M.fromList+ [ (codeIndicator_L2C, l2CMSidCode)+ , (codeIndicator_L2PD, l2PSidCode)+ ] -- | Maximum number of packed observations to allow in a single SBP message. -- maxObsPerMessage :: Int-maxObsPerMessage = floor $ (maxPayloadSize - headerSize) / packedObsSize+maxObsPerMessage = (maxPayloadSize - headerSize) `div` packedObsSize where maxPayloadSize = 255 headerSize = 7- packedObsSize = 16.0+ packedObsSize = 16 --------------------------------------------------------------------------------@@ -124,12 +129,12 @@ fromEcefVal :: Int64 -> Double fromEcefVal x = fromIntegral x / 10000 -toGPSTime :: MonadIO m => GpsObservationHeader -> m ObsGPSTime+toGPSTime :: MonadStore e m => GpsObservationHeader -> m ObsGPSTime toGPSTime hdr = do- today <- utctDay <$> liftIO getCurrentTime+ wn <- view storeWn >>= liftIO . readIORef return ObsGPSTime { _obsGPSTime_tow = hdr ^. gpsObservationHeader_tow- , _obsGPSTime_wn = fromIntegral $ div (diffDays today (fromGregorian 1980 1 6)) 7+ , _obsGPSTime_wn = wn } -- | Construct metric pseudorange (meters!) from L1 RTCM observation.@@ -169,7 +174,7 @@ lm = p + phaseRangeRes * fromIntegral (l1 ^. gpsL1Observation_carrierMinusCode) l = lm / (lightSpeedMS / l1frequency) li :: Int32- li = floor (l)+ li = floor l lf :: Word8 lf = truncate ((l - fromIntegral li) * q32Width) @@ -181,7 +186,7 @@ -> GpsL2Observation -> GpsL2ExtObservation -> CarrierPhase-toL_L2 l1 l1e l2 l2e = CarrierPhase+toL_L2 l1 l1e l2 _l2e = CarrierPhase { _carrierPhase_i = fromIntegral li , _carrierPhase_f = fromIntegral lf } where@@ -192,7 +197,7 @@ lm = p + phaseRangeRes * fromIntegral (l2 ^. gpsL2Observation_carrierMinusCode) l = lm / (lightSpeedMS / l2frequency) li :: Int32- li = floor (l)+ li = floor l lf :: Word8 lf = truncate ((l - fromIntegral li) * q32Width) @@ -210,7 +215,7 @@ -- | Construct sequenced SBP observation header ---fromGpsObservationHeader :: MonadIO m+fromGpsObservationHeader :: MonadStore e m => Word8 -- ^ Total messages -> Word8 -- ^ Message in sequence -> GpsObservationHeader -- ^ RTCM observation header@@ -234,41 +239,52 @@ fromL1SatelliteObservation sat l1 l1e = -- Checks GPS L1 code indicator for RTCM message 1002. -- See DF016, pg. 3-17 of the RTCM3 spec.- if l1 ^. gpsL1Observation_code then- PackedObsContent- { _packedObsContent_P = toP_L1 l1 l1e- , _packedObsContent_L = toL_L1 l1 l1e- , _packedObsContent_cn0 = toCn0_L1 l1e- , _packedObsContent_lock = toLock_L1 l1- , _packedObsContent_sid = GnssSignal- { _gnssSignal_sat = fromIntegral $ sat - 1- , _gnssSignal_code = l1PSidCode- , _gnssSignal_reserved = 0- }+ PackedObsContent+ { _packedObsContent_P = toP_L1 l1 l1e+ , _packedObsContent_L = toL_L1 l1 l1e+ , _packedObsContent_cn0 = toCn0_L1 l1e+ , _packedObsContent_lock = toLock_L1 l1+ , _packedObsContent_sid = GnssSignal+ { _gnssSignal_sat = fromIntegral $ sat - 1+ , _gnssSignal_code = if l1 ^. gpsL1Observation_code then l1PSidCode else l1CSidCode+ , _gnssSignal_reserved = 0 }- else- PackedObsContent- { _packedObsContent_P = toP_L1 l1 l1e- , _packedObsContent_L = toL_L1 l1 l1e- , _packedObsContent_cn0 = toCn0_L1 l1e- , _packedObsContent_lock = toLock_L1 l1- , _packedObsContent_sid = GnssSignal- { _gnssSignal_sat = fromIntegral $ sat - 1- , _gnssSignal_code = l1CSidCode- , _gnssSignal_reserved = 0- }+ }++-- | Construct an L2 SBP PackedObsContent an RTCM satellite vehicle observation+--+fromL2SatelliteObservation :: Word8 -- ^ Satellite PRN+ -> GpsL1Observation+ -> GpsL1ExtObservation+ -> GpsL2Observation+ -> GpsL2ExtObservation+ -> Maybe PackedObsContent+fromL2SatelliteObservation sat l1 l1e l2 l2e = do+ -- Checks GPS L2 code indicator.+ -- See DF016, pg. 3-17 of the RTCM3 spec.+ code <- M.lookup (l2 ^. gpsL2Observation_code) l2codeToSBPSignalCode+ return PackedObsContent+ { _packedObsContent_P = toP_L2 l1 l1e l2+ , _packedObsContent_L = toL_L2 l1 l1e l2 l2e+ , _packedObsContent_cn0 = toCn0_L2 l2e+ , _packedObsContent_lock = toLock_L2 l2+ , _packedObsContent_sid = GnssSignal+ { _gnssSignal_sat = fromIntegral $ sat - 1+ , _gnssSignal_code = code+ , _gnssSignal_reserved = 0 }+ } -- | Construct SBP GPS observation message (possibly chunked). ---chunkToMsgObs :: MonadIO m- => Msg1004 -- ^ RTCM 1004 observation message- -> Word8 -- ^ Total messages- -> Word8 -- ^ Message in sequence+chunkToMsgObs :: MonadStore e m+ => GpsObservationHeader -- ^ RTCM observation header+ -> Word8 -- ^ Total messages+ -> Word8 -- ^ Message in sequence -> [PackedObsContent] -> m MsgObs-chunkToMsgObs m totalMsgs n packed = do- header <- fromGpsObservationHeader totalMsgs n $ m ^. msg1004_header+chunkToMsgObs hdr totalMsgs n packed = do+ header <- fromGpsObservationHeader totalMsgs n hdr return MsgObs { _msgObs_header = header , _msgObs_obs = packed@@ -287,72 +303,63 @@ -- | Construct an L1 SBP PackedObsContent from an RTCM Msg 1002. ---fromObservation1002 :: Observation1002 -> Maybe PackedObsContent+fromObservation1002 :: Observation1002 -> [PackedObsContent] fromObservation1002 obs = -- Only lower set of PRN numbers (1-32) are supported- if sat > maxSats then Nothing else Just (fromL1SatelliteObservation sat l1 l1e)+ if sat > maxSats then mempty else [fromL1SatelliteObservation sat l1 l1e] where sat = obs ^. observation1002_sat l1 = obs ^. observation1002_l1 l1e = obs ^. observation1002_l1e + -- | Convert an RTCM L1 1002 observation into an SBP MsgObs. ---fromMsg1002 :: MonadIO m => Msg1002 -> m MsgObs+-- This chunking takes places because the number of observations in a given 1002+-- may very well exceed the maximum SBP supported payload size of 255 bytes.+fromMsg1002 :: MonadStore e m => Msg1002 -> m [MsgObs] fromMsg1002 m = do- header <- fromGpsObservationHeader 1 0 (m ^. msg1002_header)- return MsgObs- { _msgObs_header = header- , _msgObs_obs = mapMaybe fromObservation1002 $ m ^. msg1002_observations- }+ let hdr = m ^. msg1002_header+ obs = concatMap fromObservation1002 $ m ^. msg1002_observations+ chunks = zip [0..] $ chunksOf maxObsPerMessage obs+ totalMsgs = fromIntegral $ length chunks+ forM chunks $ uncurry $ chunkToMsgObs hdr totalMsgs -- | Construct an L1/L2 SBP PackedObsContent from an RTCM Msg 1004. ---fromObservation1004 :: Observation1004 -> Maybe [PackedObsContent]+fromObservation1004 :: Observation1004 -> [PackedObsContent] fromObservation1004 obs = -- Only lower set of PRN numbers (1-32) are supported- if sat > maxSats then Nothing else mapM id [obs1, obs2]+ if sat > maxSats then mempty else+ maybe [obs1] (: [obs1]) obs2 where sat = obs ^. observation1004_sat l1 = obs ^. observation1004_l1 l1e = obs ^. observation1004_l1e -- Checks GPS L1 code indicator for RTCM message 1004 -- See DF016, pg. 3-17 of the RTCM3 spec.- obs1 = Just (fromL1SatelliteObservation sat l1 l1e)+ obs1 = fromL1SatelliteObservation sat l1 l1e l2 = obs ^. observation1004_l2 l2e = obs ^. observation1004_l2e- code = l2 ^. gpsL2Observation_code -- Checks GPS L2 code indicator. -- See DF016, pg. 3-17 of the RTCM3 spec.- obs2 = if H.member code l2codeToSBPSignalCode then- Just PackedObsContent- { _packedObsContent_P = toP_L2 l1 l1e l2- , _packedObsContent_L = toL_L2 l1 l1e l2 l2e- , _packedObsContent_cn0 = toCn0_L2 l2e- , _packedObsContent_lock = toLock_L2 l2- , _packedObsContent_sid = GnssSignal- { _gnssSignal_sat = fromIntegral $ sat - 1- , _gnssSignal_code = l2codeToSBPSignalCode H.! code- , _gnssSignal_reserved = 0- }- }- else- Nothing+ obs2 = fromL2SatelliteObservation sat l1 l1e l2 l2e -- | Convert an RTCM L1+L2 1004 observation into multiple SBP MsgObs. -- -- This chunking takes places because the number of observations in a given 1004 -- may very well exceed the maximum SBP supported payload size of 255 bytes.-fromMsg1004 :: MonadIO m => Msg1004 -> m [MsgObs]+fromMsg1004 :: MonadStore e m => Msg1004 -> m [MsgObs] fromMsg1004 m = do- let obs = concat $ mapMaybe fromObservation1004 $ m ^. msg1004_observations+ let hdr = m ^. msg1004_header+ obs = concatMap fromObservation1004 $ m ^. msg1004_observations chunks = zip [0..] $ chunksOf maxObsPerMessage obs totalMsgs = fromIntegral $ length chunks- mapM (\(n, packed) -> chunkToMsgObs m totalMsgs n packed) chunks+ forM chunks $ uncurry $ chunkToMsgObs hdr totalMsgs -- | Convert an RTCM 1005 antenna reference position message into an SBP -- MsgBasePosEcef.-fromMsg1005 :: MonadIO m => Msg1005 -> m MsgBasePosEcef+fromMsg1005 :: MonadStore e m => Msg1005 -> m MsgBasePosEcef fromMsg1005 m = return MsgBasePosEcef { _msgBasePosEcef_x = fromEcefVal $ m ^. msg1005_reference ^. antennaReference_ecef_x@@ -362,7 +369,7 @@ -- | Convert an RTCM 1006 antenna reference position message into an SBP -- MsgBasePosEcef.-fromMsg1006 :: MonadIO m => Msg1006 -> m MsgBasePosEcef+fromMsg1006 :: MonadStore e m => Msg1006 -> m MsgBasePosEcef fromMsg1006 m = return MsgBasePosEcef { _msgBasePosEcef_x = fromEcefVal $ m ^. msg1006_reference ^. antennaReference_ecef_x@@ -372,22 +379,29 @@ -- | Convert an RTCM message into possibly multiple SBP messages. ---convert :: MonadIO m => RTCM3Msg -> m (Maybe [SBPMsg])+convert :: MonadStore e m => RTCM3Msg -> m [SBPMsg] convert = \case (RTCM3Msg1002 m _rtcm3) -> do let sender = m ^. msg1002_header ^. gpsObservationHeader_station m' <- fromMsg1002 m- return $ Just [SBPMsgObs m' $ toSBP m' $ toSender sender]+ return $ flip fmap m' $ \x -> SBPMsgObs x $ toSBP x $ toSender sender (RTCM3Msg1004 m _rtcm3) -> do let sender = m ^. msg1004_header ^. gpsObservationHeader_station m' <- fromMsg1004 m- return $ Just (fmap (\x -> SBPMsgObs x $ toSBP x $ toSender sender) m')+ return $ flip fmap m' $ \x -> SBPMsgObs x $ toSBP x $ toSender sender (RTCM3Msg1005 m _rtcm3) -> do let sender = m ^. msg1005_reference ^. antennaReference_station m' <- fromMsg1005 m- return $ Just [SBPMsgBasePosEcef m' $ toSBP m' $ toSender sender]+ return [SBPMsgBasePosEcef m' $ toSBP m' $ toSender sender] (RTCM3Msg1006 m _rtcm3) -> do let sender = m ^. msg1006_reference ^. antennaReference_station m' <- fromMsg1006 m- return $ Just [SBPMsgBasePosEcef m' $ toSBP m' $ toSender sender]- _rtcm3Msg -> return Nothing+ return [SBPMsgBasePosEcef m' $ toSBP m' $ toSender sender]+ _rtcm3Msg -> return mempty++newStore :: IO Store+newStore = do+ day <- utctDay <$> getCurrentTime+ let wn = fromIntegral $ div (diffDays day (fromGregorian 1980 1 6)) 7+ Store <$> newIORef wn+
+ src/Data/RTCM3/SBP/Types.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE ConstraintKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module: Data.RTCM3.SBP.Types+-- Copyright: Copyright (C) 2015 Swift Navigation, Inc.+-- License: LGPL-3+-- Maintainer: Mark Fine <dev@swiftnav.com>+-- Stability: experimental+-- Portability: portable+--+-- Types for RTCMv3 to SBP Conversions.++module Data.RTCM3.SBP.Types where++import BasicPrelude+import Control.Lens+import Control.Monad.Base+import Control.Monad.Catch+import Control.Monad.Reader+import Control.Monad.Trans.Control+import Control.Monad.Trans.Resource+import Data.IORef+import Data.Word++newtype ConvertT e m a = ConvertT { unConvertT :: ReaderT e m a }+ deriving+ ( Functor+ , Applicative+ , Monad+ , MonadIO+ , MonadTrans+ )++instance MonadThrow m => MonadThrow (ConvertT r m) where+ throwM = lift . throwM++instance MonadResource m => MonadResource (ConvertT r m) where+ liftResourceT = lift . liftResourceT++instance Monad m => MonadReader e (ConvertT e m) where+ ask = ConvertT ask+ local f = ConvertT . local f . unConvertT+ reader = ConvertT . reader++instance MonadTransControl (ConvertT r) where+ type StT (ConvertT r) a = StT (ReaderT r) a++ liftWith = defaultLiftWith ConvertT unConvertT+ restoreT = defaultRestoreT ConvertT++instance MonadBase b m => MonadBase b (ConvertT r m) where+ liftBase = liftBaseDefault++data Store = Store+ { _storeWn :: IORef Word16+ } deriving ( Eq )++$(makeClassy ''Store)++runConvertT :: HasStore e => e -> ConvertT e m a -> m a+runConvertT e (ConvertT m) = runReaderT m e++type MonadStore e m =+ ( MonadIO m+ , MonadReader e m+ , MonadThrow m+ , HasStore e+ )
@@ -31,10 +31,10 @@ convert :: SBPMsg -> Maybe RTCM3Msg convert = \case- (SBPMsgObs msg _sbp) -> Just $- RTCM3Msg1002 msg' $ toRTCM3 msg' where- msg' = fromMsgObs msg- (SBPMsgBasePosEcef msg _sbp) -> Just $- RTCM3Msg1005 msg' $ toRTCM3 msg' where- msg' = fromMsgBasePosEcef msg+ (SBPMsgObs m _sbp) -> Just $+ RTCM3Msg1002 m' $ toRTCM3 m' where+ m' = fromMsgObs m+ (SBPMsgBasePosEcef m _sbp) -> Just $+ RTCM3Msg1005 m' $ toRTCM3 m' where+ m' = fromMsgBasePosEcef m _sbpMsg -> Nothing
test/Test.hs view
@@ -1,6 +1,3 @@-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}- -- | -- Module: Test -- Copyright: (c) 2016 Swift Navigation Inc.
test/Test/Data/RTCM3/SBP.hs view
@@ -1,8 +1,4 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE NoImplicitPrelude #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE FlexibleContexts #-} -- | -- Module: Test.Data.RTCM3.SBP@@ -16,30 +12,30 @@ ( tests ) where -import BasicPrelude-import Control.Lens-import Control.Monad.Trans.Resource-import Data.Conduit-import Data.Conduit.Binary-import Data.Conduit.Serialization.Binary-import Data.HashMap.Strict-import Data.RTCM3.SBP-import qualified Data.Conduit.List as CL-import Data.Word-import SwiftNav.SBP-import Test.HUnit.Approx-import Test.Tasty-import Test.Tasty.HUnit+import BasicPrelude+import Control.Lens+import Control.Monad.Trans.Resource+import Data.Conduit+import Data.Conduit.Binary+import qualified Data.Conduit.List as CL+import Data.Conduit.Serialization.Binary+import Data.HashMap.Strict+import Data.IORef+import Data.RTCM3.SBP+import Data.RTCM3.SBP.Types+import Data.Word+import SwiftNav.SBP+import Test.HUnit.Approx+import Test.Tasty+import Test.Tasty.HUnit -decodeRTCMFile :: (MonadIO m, MonadBaseControl IO m, MonadThrow m)- => FilePath- -> m [SBPMsg]-decodeRTCMFile filename =- runResourceT . runConduit $- sourceFile filename =$=- conduitDecode =$=- CL.mapMaybeM convert =$=- CL.concat $$+decodeRTCMFile :: FilePath -> IO [SBPMsg]+decodeRTCMFile filename = do+ wn <- liftIO $ newIORef 1906+ runResourceT $ runConvertT (Store wn) $ runConduit $+ sourceFile filename =$=+ conduitDecode =$=+ CL.concatMapM convert $$ CL.consume basePosition :: MsgBasePosEcef -> (Double, Double, Double)@@ -160,39 +156,39 @@ assertExpectedBase (msgs !! 5) expectedBasePos -- Message 1 assertObsHeader (msgs !! 1) ObservationHeader {- _observationHeader_t = ObsGPSTime 86354000 1905+ _observationHeader_t = ObsGPSTime 86354000 1906 , _observationHeader_n_obs = 32 } assertMsgObsLength (msgs !! 1) 15 assertMsgObs (msgs !! 1) -- Message 2 assertObsHeader (msgs !! 2) ObservationHeader {- _observationHeader_t = ObsGPSTime 86354000 1905+ _observationHeader_t = ObsGPSTime 86354000 1906 , _observationHeader_n_obs = 33 } assertMsgObs (msgs !! 2) assertMsgObsLength (msgs !! 2) 1 -- Message 3 assertObsHeader (msgs !! 3) ObservationHeader {- _observationHeader_t = ObsGPSTime 86355000 1905+ _observationHeader_t = ObsGPSTime 86355000 1906 , _observationHeader_n_obs = 32 } assertMsgObsLength (msgs !! 3) 15 -- Message 4 assertObsHeader (msgs !! 4) ObservationHeader {- _observationHeader_t = ObsGPSTime 86355000 1905+ _observationHeader_t = ObsGPSTime 86355000 1906 , _observationHeader_n_obs = 33 } assertMsgObsLength (msgs !! 4) 1 -- Message 6 assertObsHeader (msgs !! 6) ObservationHeader {- _observationHeader_t = ObsGPSTime 86356000 1905+ _observationHeader_t = ObsGPSTime 86356000 1906 , _observationHeader_n_obs = 32 } assertMsgObsLength (msgs !! 6) 15 -- Message 7 assertObsHeader (msgs !! 7) ObservationHeader {- _observationHeader_t = ObsGPSTime 86356000 1905+ _observationHeader_t = ObsGPSTime 86356000 1906 , _observationHeader_n_obs = 33 } assertMsgObsLength (msgs !! 7) 1