{-# OPTIONS_GHC -Wall #-} {-# Language ScopedTypeVariables #-} {-# Language DeriveFunctor #-} module PlotHo.HistoryChannel ( Meta , XAxisType(..) , newHistoryChannel , newHistoryChannel' ) where import qualified Control.Concurrent as CC import Control.Lens ( (^.) ) import Control.Monad ( when ) import qualified Data.Foldable as F import qualified Data.IORef as IORef import Data.Time ( NominalDiffTime, getCurrentTime, diffUTCTime ) import Data.Tree ( Tree ) import qualified Data.Tree as Tree import Data.Vector ( Vector ) import qualified Data.Vector as V import qualified Data.Sequence as S import Accessors import PlotHo.Channel ( newChannel' ) import PlotHo.PlotTypes ( Channel(..), Channel'(..), SignalTree, debug ) type Meta = Tree ([String], Either String Int) newtype History a = History (S.Seq (a, Int, NominalDiffTime)) data History' = History' !Bool !(S.Seq (Double, Vector Double)) !Meta data XAxisType = XAxisTime -- ^ time since the first message | XAxisTime0 -- ^ time since the first message, normalized to 0 (to reduce plot jitter) | XAxisCount -- ^ message index | XAxisCount0 -- ^ message index, normalized to 0 (to reduce plot jitter) historySignalTree :: forall a . Lookup a => XAxisType -> String -> SignalTree (History a) historySignalTree axisType topName = makeSignalTree' [topName] accessors where makeSignalTree' :: [String] -> AccessorTree a -> SignalTree (History a) makeSignalTree' myFieldName (Right (GAData _ (GAConstructor cname children))) = Tree.Node (reverse myFieldName, Left cname) (map (\(getterName, child) -> makeSignalTree' (fromMName getterName:myFieldName) child) children) makeSignalTree' myFieldName (Right (GAData _ (GASum enum))) = Tree.Node (reverse myFieldName, Right (toHistoryGetter (fromIntegral . eToIndex enum))) [] makeSignalTree' myFieldName (Left field) = Tree.Node (reverse myFieldName, Right (toHistoryGetter (toDoubleGetter field))) [] fromMName (Just x) = x fromMName Nothing = "()" toDoubleGetter :: GAField a -> (a -> Double) toDoubleGetter (FieldDouble f) = (^. f) toDoubleGetter (FieldFloat f) = realToFrac . (^. f) toDoubleGetter (FieldInt f) = fromIntegral . (^. f) toDoubleGetter (FieldString _) = const 0 toDoubleGetter FieldSorry = const 0 toHistoryGetter :: (a -> Double) -> History a -> [[(Double, Double)]] toHistoryGetter = case axisType of XAxisTime -> timeGetter XAxisTime0 -> timeGetter0 XAxisCount -> countGetter XAxisCount0 -> countGetter0 timeGetter get (History s) = [map (\(val, _, time) -> (realToFrac time, get val)) (F.toList s)] timeGetter0 get (History s) = [map (\(val, _, time) -> (realToFrac time - time0, get val)) (F.toList s)] where time0 :: Double time0 = case S.viewl s of (_, _, time0') S.:< _ -> realToFrac time0' S.EmptyL -> 0 countGetter get (History s) = [map (\(val, k, _) -> (fromIntegral k, get val)) (F.toList s)] countGetter0 get (History s) = [map (\(val, k, _) -> (fromIntegral k - k0, get val)) (F.toList s)] where k0 :: Double k0 = case S.viewl s of (_, k0', _) S.:< _ -> realToFrac k0' S.EmptyL -> 0 -- | Simplified time-series channel which automatically generates the signal tree -- based on 'Accessors.Lookup'. -- You have to recompile the plotter if the types change. -- The plotter will plot a time series of messages put by the action returned by this function. -- The worker should pass True to reset the message history, so sending True the first message and False subsequent messages is a good starting place. -- If this is too restrictive, use the more generic 'newChannel' -- and use a Tree-like type to represent your data, or use 'newHistoryChannel''. newHistoryChannel :: forall a . Lookup a => String -- ^ channel name -> XAxisType -- ^ what to use for the X axis -> IO (Channel, a -> Bool -> IO ()) -- ^ return a channel and a "new message" action which can also reset the history newHistoryChannel name xaxisType = do time0 <- getCurrentTime >>= IORef.newIORef counter <- IORef.newIORef 0 let toSignalTree :: History a -> SignalTree (History a) toSignalTree = const (historySignalTree xaxisType name) sameSignalTree _ _ = True (channel', newHistoryMessage) <- newChannel' name sameSignalTree toSignalTree :: IO (Channel' (History a), History a -> IO ()) -- Put the first message immediately in order to immediately build the signal tree -- so that the user can see it without waiting for the first message. newHistoryMessage (History mempty) let newMessage :: a -> Bool -> IO () newMessage next reset = do debug "newMessage(newHistoryChannel): message received" -- grab the time and counter time <- getCurrentTime when reset $ do IORef.writeIORef time0 time IORef.writeIORef counter 0 k <- IORef.readIORef counter time0' <- IORef.readIORef time0 IORef.writeIORef counter (k+1) let val = (next, k, diffUTCTime time time0') latestChanValue <- CC.readMVar (chanLatestValueMVar channel') oldTimeSeries <- case latestChanValue of Just (History r, _) -> return r Nothing -> error "newMessage(newHistoryChannel): the 'impossible' happened: channel has no latest message" maxHistory <- IORef.readIORef (chanMaxHistory channel') let newTimeSeries | reset = S.singleton val | otherwise = S.drop (1 + S.length oldTimeSeries - maxHistory) (oldTimeSeries S.|> val) debug "newMessage(newHistoryChannel): new history message calling internal newMessage" newHistoryMessage (History newTimeSeries) clearHistory :: History a -> History a clearHistory = const (History mempty) return (Channel (channel' {chanClearHistory = Just clearHistory}) , newMessage) -- | History channel which supports data whose structure can change. -- It does NOT automatically generate the signal tree like 'newHistoryChannel' does. -- This returns a channel and an action which takes x-axis value, a vector of y axis values, and a tree which -- indexes these y axis values. -- If the data structure changes, a new tree should be sent, otherwise there could be indexing errors. newHistoryChannel' :: String -- ^ channel name -> IO (Channel, Either Meta (Double, Vector Double) -> IO ()) newHistoryChannel' name = do let toSignalTree :: History' -> Tree ( [String] , Either String (History' -> [[(Double, Double)]]) ) toSignalTree (History' _ _ meta) = fmap f meta where f :: ([String], Either String Int) -> ([String], Either String (History' -> [[(Double, Double)]])) f (n0, Left n1) = (n0, Left n1) f (n0, Right k) = (n0, Right g) where g :: History' -> [[(Double, Double)]] g (History' _ vals _) = [map toVal (F.toList vals)] where toVal (t, x) = (t, x V.! k) sameSignalTree :: History' -> History' -> Bool -- assume the signal trees are the same if it's not a reset sameSignalTree (History' _ _ _) (History' False _ _) = True -- if it's a reset, then compare the signal trees sameSignalTree (History' _ _ old) (History' True _ new) = old == new (channel', newHistoryMessage) <- newChannel' name sameSignalTree toSignalTree :: IO (Channel' History', History' -> IO ()) let newMessage :: Either Meta (Double, Vector Double) -> IO () newMessage msg = do latestChannelValue <- CC.readMVar (chanLatestValueMVar channel') case (latestChannelValue, msg) of -- first message and no meta to go with it (Nothing, Right _) -> putStr $ unlines [ "WARNING: First message seen by Plot-ho-matic doesn't have signal tree meta-data." , "This was probably caused by starting the plotter AFTER sending the first telemetry message." , "Try restarting the application sending messages." ] -- any message with meta is a reset (_, Left meta) -> newHistoryMessage (History' True mempty meta) -- later message without meta - no reset (Just (History' _ oldTimeSeries meta, _), Right (nextTime, nextVal)) -> do maxHistory <- IORef.readIORef (chanMaxHistory channel') let newTimeSeries = S.drop (1 + S.length oldTimeSeries - maxHistory) (oldTimeSeries S.|> (nextTime, nextVal)) newHistoryMessage (History' False newTimeSeries meta) clearHistory :: History' -> History' clearHistory (History' reset _ meta) = History' reset mempty meta return (Channel (channel' {chanClearHistory = Just clearHistory}) , newMessage)