{-# LANGUAGE OverloadedStrings, TemplateHaskell #-} {-| Ham radio logging and other operations. This module provides a monad that abstracts some of the work necessary for logging ham radio contacts, as well as doing related tasks. -} module Ham.Log (module Ham.Qso, -- * Reading and writing log and configuration. logToFile, logFromFile, configToFile, configFromFile, -- * The HamLog monad. HamLog, LogState(..), emptyLogState, -- ** Configuration LogConfigV3(..), LogConfig(..), configLogFile, configQsoDefaults, defaultConfig, -- ** Running the monad evalHamLog, execHamLog, runHamLog, -- ** Reading and writing logs readLog, writeLog, -- ** Manipulating QSOs. addQso, newQsoNow, fillFromCAT, updateQso, deleteQso, sortLog, -- ** Helpers currentUtcTime, -- ** Queries filterCallsign, isDuplicate, findQsos, findDuplicateQsos, Duplicate(..), getQsoList, getQsoSeq, asks, -- ** Interacting with a radio via CAT cat, -- ** Getting information from the FCC database. lookupFcc, lookupFccName, -- * The Log Log(..), addQsoToLog, sortLog_, emptyLog, -- * Exporting makeCabrillo ) where import Data.Foldable import qualified Data.Sequence as S import Data.Sequence (Seq) import Data.Time.Calendar import Data.Time.Clock import Data.Time.LocalTime import Data.Text as T import Data.Sequence import qualified Data.ByteString.Lazy as B import Data.Aeson.Encode.Pretty import Data.Aeson import Data.Maybe (isJust, fromJust) import qualified Data.Map as M import Control.Applicative ((<|>)) import Control.Monad (when) import Control.Monad.Trans.RWS.Strict import Control.Monad.IO.Class import Control.Exception import Ham.Internal.Log import Ham.Internal.Log.Config import Ham.Fcc import Ham.Cabrillo import Ham.Qso import Ham.Data import qualified Ham.CAT as CAT import Ham.CAT.Radios import Lens.Micro.TH -- | An empty log. emptyLog :: Log emptyLog = Log mempty -- | Add a QSO to the beginning of the log list. addQsoToLog :: Log -> Qso -> Log addQsoToLog l q = l { _logQsos = m } where m = q <| _logQsos l sortLog_ :: Ord a => Log -> (Qso -> a) -> Log sortLog_ l g = l { _logQsos = sortBy f (_logQsos l) } where f a b = compare (g a) (g b) -- | Write a log to a file in JSON format. logToFile :: Log -> FilePath -> IO () logToFile l fp = B.writeFile fp $ encodePretty l -- | Read a log from a file in JSON format. logFromFile :: FilePath -> IO (Maybe Log) logFromFile fp = (decode <$> B.readFile fp) `catch` \(SomeException e) -> return Nothing -------------------------------------------------------------------------------- -- Log monad. {-| Log monad. You would normally run the monad for example with 'evalHamLog', and use the monadic actions such as 'readLog', 'newQsoNow', 'writeLog', etc. to modify the log and QSOs. -} type HamLog = RWST LogConfig [Text] LogState IO data LogState = LogState { _stateLog :: Log , _stateUseCat :: Bool -- ^ Mirroring the configuration in _configCat in the LogConfig to be able to turn it off after a failed action. , _stateSerialCAT :: Maybe CAT.SerialCAT } makeLenses ''LogState emptyLogState :: LogState emptyLogState = LogState emptyLog True Nothing initState :: HamLog () initState = do catconf <- asks _configCat -- Set the serial radio interface if there is one. let mserial = M.lookup (CAT.catRadio catconf) serialInterface s <- get c <- asks _configUseCat put s { _stateSerialCAT = mserial , _stateUseCat = c } -- maybe (return ()) (\serial -> do { s <- get; put s { _stateSerialCAT = Just serial } }) mserial -- | Run a 'HamLog' action and return the result and potential logging text. evalHamLog :: LogConfig -> LogState -> HamLog a -> IO (a, [Text]) evalHamLog cfg s act = evalRWST (initState >> act) cfg s -- | Run a 'HamLog' action and return the final state and potential logging text. execHamLog :: LogConfig -> LogState -> HamLog a -> IO (LogState, [Text]) execHamLog cfg s act = execRWST (initState >> act) cfg s -- | Run a 'HamLog' action and return the result, final state, and potential logging text. runHamLog :: LogConfig -> LogState -> HamLog a -> IO (a, LogState, [Text]) runHamLog cfg s act = runRWST (initState >> act) cfg s -- | Read the log database from the file set in '_configLogFile' entry of the 'LogConfig'. -- The log is then set to the '_stateLog' entry of the current 'LogState'. readLog :: HamLog () readLog = do f <- asks _configLogFile ml <- liftIO $ logFromFile f `catch` \(SomeException a) -> return Nothing maybe (return ()) (\l -> do { s <- get; put $ s { _stateLog = l } }) ml -- | Write the log database to the file set in '_configLogFile' entry of the 'LogConfig'. writeLog :: HamLog () writeLog = do f <- asks _configLogFile l <- gets _stateLog liftIO $ logToFile l f -- | Add the given QSO at the front of the current log. addQso :: Qso -> HamLog () addQso q = do l <- gets _stateLog let l' = addQsoToLog l $ sanitizeQso q modify $ \s -> s { _stateLog = l' } -- | Get the current UTC time up to seconds. currentUtcTime :: HamLog UTCTime currentUtcTime = liftIO $ do t <- getCurrentTime let tod = utctDayTime t -- Remove anything that is more accurate than one second. We assume that is enough for -- our purpose. tod' = (picosecondsToDiffTime . (\a -> a - a `rem` (10^12)) . diffTimeToPicoseconds) tod return $ t { utctDayTime = tod' } -- | Create a new QSO with the current UTC time as reported by the operating system, -- and set the default values from the '_configQsoDefaults' values. -- uses `fillFromCAT` to fill some values from a connected radio, if any. newQsoNow :: HamLog Qso newQsoNow = do now <- currentUtcTime defaults <- asks _configQsoDefaults let q = (qsoWithDefaults defaults) { _qsoTimeStart = now , _qsoTimeEnd = (fromIntegral 60) `addUTCTime` now } q' <- fillFromCAT q addQso q' return q' -- | Try to get a few values from a CAT connected radio. -- If the function is turned off (_stateUseCat is False), then the input is going to be -- returned unchanged. The same is true if there is an error while retrieving -- data from the radio. -- Note that when the functionality is turned on and there is an error, it may take a few seconds -- for the function to return. A message will be set in the Writer layer of HamLog, -- so that you can get the messages back with `evalHamLog`, `execHamLog`, or `runHamLog`. fillFromCAT :: Qso -> HamLog Qso fillFromCAT qso = do result <- cat $ do f <- CAT.catFrequency m <- CAT.catMode return (f,m) case result of Left (CAT.CATError t s) -> tell [s] >> return qso Right ((mf, mm), _) -> return $ qso { _qsoFrequency = maybe (_qsoFrequency qso) id mf, _qsoMode = maybe (_qsoMode qso) id mm } -- | Update the QSO at the given position in the log with the given QSO. updateQso :: Int -> Qso -> HamLog () updateQso i q = do l <- gets _stateLog let q' = update i (sanitizeQso q) $ _logQsos l modify $ \s -> s { _stateLog = l { _logQsos = q' } } return () -- | Delete the QSO at the given position in the log. deleteQso :: Int -> HamLog () deleteQso i = do l <- gets _stateLog let q' = deleteAt i $ _logQsos l modify $ \s -> s { _stateLog = l { _logQsos = q' } } -- | Sort the log according to the given key. sortLog :: Ord a => (Qso -> a) -> HamLog () sortLog g = do l <- gets _stateLog let l' = sortLog_ l g modify $ \s -> s { _stateLog = l' } -- | Get a list of QSOs. Use qsoSeq for better performance. getQsoList :: HamLog [Qso] getQsoList = do l <- _logQsos <$> gets _stateLog return $ Data.Foldable.foldr (:) [] l -- | Get the sequence of stored QSOs. getQsoSeq :: HamLog (Seq Qso) getQsoSeq = _logQsos <$> gets _stateLog -- | Return a sequence of all QSOs with a given callsign. filterCallsign :: Text -- ^ The Callsign to find. -> HamLog (Seq Qso) -- ^ Returns a sequence of Qsos. filterCallsign callsign = do l <- getQsoSeq let f q = let cs = T.toUpper $ _qsoCallsign q cs' = T.strip $ T.toUpper callsign in cs == cs' return (S.filter f l) -- | Checks if there is already at least one entry with the given callsign. -- DEPRECATED. Use findDuplicateQsos. isDuplicate :: Text -- ^ The Callsign to find. -> HamLog Bool isDuplicate callsign = do s <- filterCallsign callsign case s of Empty -> return False _ -> return True -- | Find qsos containing the given information. findQsos :: Text -- ^ The callsign to find. -> Maybe Band -- ^ Band to check for -> Maybe QsoMode -- ^ Mode to check for -> HamLog (Seq Qso) findQsos callsign mband mmode = do s <- filterCallsign callsign let msb = maybe Nothing (\a -> Just $ S.filter (\qso -> band (_qsoFrequency qso) == a) s) mband Just s2 = msb <|> Just s msm = maybe Nothing (\a -> Just $ S.filter (\qso -> _qsoMode qso == a) s2) mmode Just s3 = msm <|> Just s2 return s3 data Duplicate = Duplicate { duplicateBand :: Bool -- ^ Indicates whether band matches , duplicateMode :: Bool -- ^ Indicates whether mode matches , duplicateQso :: Qso -- ^ The potential duplicate QSO } instance Show Duplicate where show d = "Met " ++ c ++ " on " ++ show b ++ " using " ++ show m ++ " at " ++ show (_qsoTimeStart q) where b = band $ _qsoFrequency q m = _qsoMode q q = duplicateQso d c = T.unpack $ _qsoCallsign q findDuplicateQsos :: Qso -> HamLog (Seq Duplicate) findDuplicateQsos qso = do let qso' = sanitizeQso qso b = band $ _qsoFrequency qso' c = _qsoCallsign qso' m = _qsoMode qso' f a = if _qsoCallsign a == c && (a /= qso') -- Do not return the same QSO itself. FIXME: It may be necessary to use IDs for this. then Just (Duplicate { duplicateBand = band (_qsoFrequency a) == b, duplicateMode = _qsoMode a == m, duplicateQso = a }) else Nothing catM :: S.Seq (Maybe a) -> S.Seq a catM s = fmap fromJust $ S.filter isJust s qsos <- getQsoSeq return $ catM $ fmap f qsos -- | Get the FCC information for a given callsign. lookupFcc :: Text -> HamLog (Maybe FccResult) lookupFcc a = liftIO $ fccLookup' a -- | Get the operator's name for a given callsign from the FCC. lookupFccName :: Text -> HamLog Text lookupFccName a = liftIO $ fccLookupName a -- | Make a cabrillo log from the current log, given a cabrillo configuration. makeCabrillo :: Cabrillo -> HamLog CabrilloLog makeCabrillo cab = do l <- getQsoList myCall <- _qsoDefaultCallsign <$> asks _configQsoDefaults let l' = Prelude.concatMap toCabrillo l return $ CabrilloLog cab l' -- | Run a CAT action. cat :: CAT.CAT IO a -> HamLog (Either CAT.CATError (a, [Text])) cat act = do -- FIXME: fromJust may be somewhat dangerous. As it currently is used, -- it wil always be set, but that is not guaranteed to remain this way. usecat <- gets _stateUseCat if usecat then do radio_interface <- fromJust <$> gets _stateSerialCAT conf <- asks _configCat let catstate = CAT.defaultState { CAT.stateInterface = radio_interface } result <- liftIO $ CAT.runCAT conf catstate act case result of Left (CAT.CATError t s) -> do tell $ ["CAT failed: " <> s <> " - turning off CAT for now."] s <- get put $ s { _stateUseCat = False } Right a -> tell $ snd a return result else return $ Left $ (CAT.CATError CAT.CATErrorGeneric "CAT is turned off.")