gnss-converters 0.1.12 → 0.1.13
raw patch · 5 files changed
+222/−60 lines, 5 filesdep +random
Dependencies added: random
Files
- gnss-converters.cabal +3/−1
- main/RTCM32SBP.hs +1/−1
- src/Data/RTCM3/SBP.hs +143/−54
- src/Data/RTCM3/SBP/Types.hs +18/−1
- test/Test/Data/RTCM3/SBP.hs +57/−3
gnss-converters.cabal view
@@ -1,5 +1,5 @@ name: gnss-converters-version: 0.1.12+version: 0.1.13 synopsis: GNSS Converters. description: Haskell bindings for GNSS converters. homepage: http://github.com/swift-nav/gnss-converters@@ -29,6 +29,7 @@ , lens , monad-control , mtl+ , random , resourcet , rtcm , sbp@@ -80,6 +81,7 @@ , gnss-converters , lens , resourcet+ , rtcm , sbp , tasty , tasty-hunit
main/RTCM32SBP.hs view
@@ -20,7 +20,7 @@ main :: IO () main = do- s <- liftIO $ newStore+ s <- newStore runResourceT $ runConvertT s $ sourceHandle stdin =$= conduitDecode =$=
src/Data/RTCM3/SBP.hs view
@@ -14,6 +14,7 @@ import BasicPrelude import Control.Lens+import Control.Monad.Extra import Data.Bits import qualified Data.HashMap.Strict as M import Data.IORef@@ -23,6 +24,7 @@ import Data.Time import Data.Word import SwiftNav.SBP+import System.Random --------------------------------------------------------------------------------@@ -123,20 +125,65 @@ --------------------------------------------------------------------------------+-- General utilities+++modifyMap :: (Eq k, Hashable k) => IORef (HashMap k v) -> v -> k -> (v -> v) -> IO v+modifyMap r d k a = do+ m <- readIORef r+ let v = a $ M.lookupDefault d k m+ n = M.insert k v m+ writeIORef r n+ n `seq` return v++maybe' :: Maybe a -> b -> (a -> b) -> b+maybe' m b a = maybe b a m+++-------------------------------------------------------------------------------- -- GNSS RTCM observation reconstruction utilities fromEcefVal :: Int64 -> Double fromEcefVal x = fromIntegral x / 10000 -toGPSTime :: MonadStore e m => GpsObservationHeader -> m ObsGPSTime-toGPSTime hdr = do+newGPSTime :: MonadStore e m => Word32 -> m ObsGPSTime+newGPSTime tow = do wn <- view storeWn >>= liftIO . readIORef return ObsGPSTime- { _obsGPSTime_tow = hdr ^. gpsObservationHeader_tow+ { _obsGPSTime_tow = tow , _obsGPSTime_wn = wn } +-- | If incoming TOW is less than stored TOW, rollover WN.+--+updateGPSTime :: Word32 -> ObsGPSTime -> ObsGPSTime+updateGPSTime tow gpsTime =+ gpsTime & obsGPSTime_tow .~ tow &+ if tow >= gpsTime ^. obsGPSTime_tow then id else+ obsGPSTime_wn %~ (+ 1)++-- | Produce GPS Time from Observation header, handling WN rollover.+--+toGPSTime :: MonadStore e m => GpsObservationHeader -> m ObsGPSTime+toGPSTime hdr = do+ let tow = hdr ^. gpsObservationHeader_tow+ station = hdr ^. gpsObservationHeader_station+ gpsTime <- newGPSTime tow+ gpsTimeMap <- view storeGPSTimeMap+ liftIO $ modifyMap gpsTimeMap gpsTime station $ updateGPSTime tow++-- | MJD GPS Epoch - First day in GPS week 0. See DF051 of the RTCM3 spec+--+mjdEpoch :: Word16+mjdEpoch = 44244++-- | Convert from MJD to GPS week number+--+-- See DF051 of the RTCM3 spec+toWn :: Word16 -> Word16+toWn mjd = (mjd - mjdEpoch) `div` 7+ -- | Construct metric pseudorange (meters!) from L1 RTCM observation. -- -- See DF011, pg. 3-16 of the RTCM3 spec@@ -207,12 +254,31 @@ toCn0_L2 :: GpsL2ExtObservation -> Word8 toCn0_L2 = (^. gpsL2ExtObservation_cnr) -toLock_L1 :: GpsL1Observation -> Word16-toLock_L1 _l1 = 0+newLock :: IO Lock+newLock = do+ counter <- randomIO+ return Lock+ { _lockTime = 0+ , _lockCounter = counter+ } -toLock_L2 :: GpsL2Observation -> Word16-toLock_L2 _l2 = 0+-- | If incoming time is less than stored time, increment lock counter.+--+updateLock :: Word8 -> Lock -> Lock+updateLock time lock =+ lock & lockTime .~ time &+ if time >= lock ^. lockTime then id else+ lockCounter %~ (+1) +-- | Produce Lock Counter from Lock Time, handling cycle slips.+--+toLock :: MonadStore e m => Word16 -> GnssSignal -> Word8 -> m Word16+toLock station sid time = do+ lockMap <- view storeLockMap+ lock <- liftIO $ newLock+ liftIO $ fmap (^. lockCounter) $+ modifyMap lockMap lock (station, sid) $ updateLock time+ -- | Construct sequenced SBP observation header -- fromGpsObservationHeader :: MonadStore e m@@ -230,50 +296,70 @@ , _observationHeader_n_obs = totalMsgs `shiftL` 4 .|. n } +-- | Construct an L1 GnssSignal+--+toL1GnssSignal :: Word8 -> GpsL1Observation -> GnssSignal+toL1GnssSignal sat l1 =+ GnssSignal+ { _gnssSignal_sat = fromIntegral $ sat - 1+ , _gnssSignal_code = if l1 ^. gpsL1Observation_code then l1PSidCode else l1CSidCode+ , _gnssSignal_reserved = 0+ }+ -- | Construct an L1 SBP PackedObsContent an RTCM satellite vehicle observation ---fromL1SatelliteObservation :: Word8 -- ^ Satellite PRN+fromL1SatelliteObservation :: MonadStore e m+ => Word16 -- ^ Station ID+ -> Word8 -- ^ Satellite PRN -> GpsL1Observation -> GpsL1ExtObservation- -> PackedObsContent-fromL1SatelliteObservation sat l1 l1e =+ -> m PackedObsContent+fromL1SatelliteObservation station sat l1 l1e = do -- Checks GPS L1 code indicator for RTCM message 1002. -- See DF016, pg. 3-17 of the RTCM3 spec.- PackedObsContent+ let sid = toL1GnssSignal sat l1+ lock <- toLock station sid $ l1 ^. gpsL1Observation_lockTime+ return 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- }+ , _packedObsContent_lock = lock+ , _packedObsContent_sid = sid } +-- | Construct an L2 GnssSignal+--+toL2GnssSignal :: Word8 -> GpsL2Observation -> Maybe GnssSignal+toL2GnssSignal sat l2 = do+ code <- M.lookup (l2 ^. gpsL2Observation_code) l2codeToSBPSignalCode+ return GnssSignal+ { _gnssSignal_sat = fromIntegral $ sat - 1+ , _gnssSignal_code = code+ , _gnssSignal_reserved = 0+ }+ -- | Construct an L2 SBP PackedObsContent an RTCM satellite vehicle observation ---fromL2SatelliteObservation :: Word8 -- ^ Satellite PRN+fromL2SatelliteObservation :: MonadStore e m+ => Word16 -- ^ Station ID+ -> Word8 -- ^ Satellite PRN -> GpsL1Observation -> GpsL1ExtObservation -> GpsL2Observation -> GpsL2ExtObservation- -> Maybe PackedObsContent-fromL2SatelliteObservation sat l1 l1e l2 l2e = do+ -> m (Maybe PackedObsContent)+fromL2SatelliteObservation station sat l1 l1e l2 l2e = -- 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+ maybe' (toL2GnssSignal sat l2) (return Nothing) $ \sid -> do+ lock <- toLock station sid $ l2 ^. gpsL2Observation_lockTime+ return $ Just PackedObsContent+ { _packedObsContent_P = toP_L2 l1 l1e l2+ , _packedObsContent_L = toL_L2 l1 l1e l2 l2e+ , _packedObsContent_cn0 = toCn0_L2 l2e+ , _packedObsContent_lock = lock+ , _packedObsContent_sid = sid }- } -- | Construct SBP GPS observation message (possibly chunked). --@@ -303,47 +389,45 @@ -- | Construct an L1 SBP PackedObsContent from an RTCM Msg 1002. ---fromObservation1002 :: Observation1002 -> [PackedObsContent]-fromObservation1002 obs =+fromObservation1002 :: MonadStore e m => Word16 -> Observation1002 -> m [PackedObsContent]+fromObservation1002 station obs = -- Only lower set of PRN numbers (1-32) are supported- if sat > maxSats then mempty else [fromL1SatelliteObservation sat l1 l1e]+ if sat > maxSats then return mempty else do+ obs1 <- fromL1SatelliteObservation station sat l1 l1e+ return [obs1] where sat = obs ^. observation1002_sat l1 = obs ^. observation1002_l1 l1e = obs ^. observation1002_l1e - -- | Convert an RTCM L1 1002 observation into an SBP 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- let hdr = m ^. msg1002_header- obs = concatMap fromObservation1002 $ m ^. msg1002_observations- chunks = zip [0..] $ chunksOf maxObsPerMessage obs+ let hdr = m ^. msg1002_header+ station = hdr ^. gpsObservationHeader_station+ obs <- concatMapM (fromObservation1002 station) $ m ^. msg1002_observations+ let 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 -> [PackedObsContent]-fromObservation1004 obs =+fromObservation1004 :: MonadStore e m => Word16 -> Observation1004 -> m [PackedObsContent]+fromObservation1004 station obs = -- Only lower set of PRN numbers (1-32) are supported- if sat > maxSats then mempty else- maybe [obs1] (: [obs1]) obs2+ if sat > maxSats then return mempty else do+ obs1 <- fromL1SatelliteObservation station sat l1 l1e+ obs2 <- fromL2SatelliteObservation station sat l1 l1e l2 l2e+ return $ 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 = fromL1SatelliteObservation sat l1 l1e- l2 = obs ^. observation1004_l2- l2e = obs ^. observation1004_l2e- -- Checks GPS L2 code indicator.- -- See DF016, pg. 3-17 of the RTCM3 spec.- obs2 = fromL2SatelliteObservation sat l1 l1e l2 l2e+ l2 = obs ^. observation1004_l2+ l2e = obs ^. observation1004_l2e -- | Convert an RTCM L1+L2 1004 observation into multiple SBP MsgObs. --@@ -351,9 +435,10 @@ -- may very well exceed the maximum SBP supported payload size of 255 bytes. fromMsg1004 :: MonadStore e m => Msg1004 -> m [MsgObs] fromMsg1004 m = do- let hdr = m ^. msg1004_header- obs = concatMap fromObservation1004 $ m ^. msg1004_observations- chunks = zip [0..] $ chunksOf maxObsPerMessage obs+ let hdr = m ^. msg1004_header+ station = hdr ^. gpsObservationHeader_station+ obs <- concatMapM (fromObservation1004 station) $ m ^. msg1004_observations+ let chunks = zip [0..] $ chunksOf maxObsPerMessage obs totalMsgs = fromIntegral $ length chunks forM chunks $ uncurry $ chunkToMsgObs hdr totalMsgs @@ -397,11 +482,15 @@ let sender = m ^. msg1006_reference ^. antennaReference_station m' <- fromMsg1006 m return [SBPMsgBasePosEcef m' $ toSBP m' $ toSender sender]+ (RTCM3Msg1013 m _rtcm3) -> do+ wn <- view storeWn+ liftIO $ writeIORef wn $ toWn $ m ^. msg1013_header ^. messageHeader_mjd+ return mempty _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+ Store <$> newIORef wn <*> newIORef mempty <*> newIORef mempty
src/Data/RTCM3/SBP/Types.hs view
@@ -1,3 +1,4 @@+{-# OPTIONS -fno-warn-orphans #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -27,7 +28,21 @@ import Control.Monad.Trans.Resource import Data.IORef import Data.Word+import SwiftNav.SBP +data Lock = Lock+ { _lockTime :: Word8+ , _lockCounter :: Word16+ } deriving ( Eq )++$(makeLenses ''Lock)++type GPSTimeMap = HashMap Word16 ObsGPSTime+type LockMap = HashMap (Word16, GnssSignal) Lock++instance Hashable GnssSignal where+ hashWithSalt s g = hashWithSalt s (g ^. gnssSignal_sat, g ^. gnssSignal_code, g ^. gnssSignal_reserved)+ newtype ConvertT e m a = ConvertT { unConvertT :: ReaderT e m a } deriving ( Functor@@ -58,7 +73,9 @@ liftBase = liftBaseDefault data Store = Store- { _storeWn :: IORef Word16+ { _storeWn :: IORef Word16+ , _storeLockMap :: IORef LockMap+ , _storeGPSTimeMap :: IORef GPSTimeMap } deriving ( Eq ) $(makeClassy ''Store)
test/Test/Data/RTCM3/SBP.hs view
@@ -31,8 +31,8 @@ decodeRTCMFile :: FilePath -> IO [SBPMsg] decodeRTCMFile filename = do- wn <- liftIO $ newIORef 1906- runResourceT $ runConvertT (Store wn) $ runConduit $+ s <- Store <$> newIORef 1906 <*> newIORef mempty <*> newIORef mempty+ runResourceT $ runConvertT s $ runConduit $ sourceFile filename =$= conduitDecode =$= CL.concatMapM convert $$@@ -42,7 +42,7 @@ basePosition msg' = ( msg' ^. msgBasePosEcef_x , msg' ^. msgBasePosEcef_y , msg' ^. msgBasePosEcef_z- )+ ) isL1 :: PackedObsContent -> Bool isL1 obs = (obs ^. packedObsContent_sid ^. gnssSignal_code) == l1CSidCode@@ -194,8 +194,62 @@ assertMsgObsLength (msgs !! 7) 1 ] +testToWn :: TestTree+testToWn =+ testGroup "MJD to GPS week number"+ [ testCase "Day 0, Week 0" $+ toWn mjdEpoch @?= 0+ , testCase "Day 4, Week 1906" $+ toWn 57590 @?= 1906+ , testCase "Day 0, Week 1906" $+ toWn 57586 @?= 1906+ , testCase "Day 7, Week 1905" $+ toWn 57585 @?= 1905+ , testCase "Day 7, Week 1906" $+ toWn 57592 @?= 1906+ , testCase "Day 0, Week 1907" $+ toWn 57593 @?= 1907+ ]++testUpdateGPSTime :: TestTree+testUpdateGPSTime =+ testGroup "Update GPS Time"+ [ testCase "old TOW < new TOW" $ do+ let old = ObsGPSTime 1 10+ new = updateGPSTime 2 old+ new ^. obsGPSTime_wn @?= 10+ , testCase "old TOW = new TOW" $ do+ let old = ObsGPSTime 1 10+ new = updateGPSTime 1 old+ new ^. obsGPSTime_wn @?= 10+ , testCase "old TOW > new TOW" $ do+ let old = ObsGPSTime 1 10+ new = updateGPSTime 0 old+ new ^. obsGPSTime_wn @?= 11+ ]++testUpdateLock :: TestTree+testUpdateLock =+ testGroup "Update Lock"+ [ testCase "old time < new time" $ do+ let old = Lock 1 10+ new = updateLock 2 old+ new ^. lockCounter @?= 10+ , testCase "old time = new time" $ do+ let old = Lock 1 10+ new = updateLock 1 old+ new ^. lockCounter @?= 10+ , testCase "old time > new time" $ do+ let old = Lock 1 10+ new = updateLock 0 old+ new ^. lockCounter @?= 11+ ]+ tests :: TestTree tests = testGroup "RTCM3 to SBP conversion tests" [ testMsg1004+ , testToWn+ , testUpdateGPSTime+ , testUpdateLock ]