ogma-core-1.10.0: src/Command/ROSApp.hs
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- Copyright 2022 United States Government as represented by the Administrator
-- of the National Aeronautics and Space Administration. All Rights Reserved.
--
-- Disclaimers
--
-- Licensed under the Apache License, Version 2.0 (the "License"); you may
-- not use this file except in compliance with the License. You may obtain a
-- copy of the License at
--
-- https://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-- License for the specific language governing permissions and limitations
-- under the License.
--
-- | Create <https://www.ros.org/ Robot Operating System> (ROS) applications
-- that subscribe to obtain data and call Copilot when new values arrive.
--
-- It is the user's responsibility to modify the generated Copilot/C/C++ code
-- to deal with the monitors they'd like to implement, and the data they must
-- manipulate.
{- HLINT ignore "Functor law" -}
module Command.ROSApp
( command
, CommandOptions(..)
, Node(Node)
, ErrorCode
)
where
-- External imports
import Control.Applicative (liftA2, (<|>))
import qualified Control.Exception as E
import Control.Monad.Except (ExceptT (..), liftEither)
import Data.Aeson (ToJSON (..))
import Data.Maybe (fromMaybe, mapMaybe, maybeToList)
import GHC.Generics (Generic)
-- External imports: auxiliary
import System.Directory.Extra (copyTemplate)
import qualified Command.Standalone
-- Internal imports: auxiliary
import Command.Result (Result (..))
-- Internal imports
import Command.Common
import Command.Errors (ErrorCode, ErrorTriplet (..))
import Command.VariableDB (Connection (..), InputDef (..), TopicDef (..),
TypeDef (..), VariableDB, findConnection, findInput,
findTopic, findType, findTypeByType)
-- | Generate a new ROS application connected to Copilot.
command :: CommandOptions -- ^ Options to the ROS backend.
-> IO (Result ErrorCode)
command options = processResult $ do
-- Obtain template dir
templateDir <- locateTemplateDir mTemplateDir "ros"
templateVars <- parseTemplateVarsFile templateVarsF
appData <- command' options functions
let subst = mergeObjects (toJSON appData) templateVars
-- Expand template
ExceptT $ fmap (makeLeftE cannotCopyTemplate) $ E.try $
copyTemplate templateDir subst targetDir
where
targetDir = commandTargetDir options
mTemplateDir = commandTemplateDir options
functions = exprPair (commandPropFormat options)
templateVarsF = commandExtraVars options
command' :: CommandOptions
-> ExprPair
-> ExceptT ErrorTriplet IO AppData
command' options (ExprPair exprT) = do
-- Open files needed to fill in details in the template.
vs <- parseVariablesFile varNameFile
rs <- parseRequirementsListFile handlersFile
varDB <- openVarDBFilesWithDefault varDBFile
specT <- maybe (return Nothing) (\e -> Just <$> parseInputExpr' e) cExpr
specF <- maybe (return Nothing) (\f -> Just <$> parseInputFile' f) fp
let spec = specT <|> specF
liftEither $ checkArguments spec vs rs
copilotM <- sequenceA $ (\spec' -> processSpec spec' fp cExpr) <$> spec
let varNames = fromMaybe (specExtractExternalVariables spec) vs
monitors = maybe
(specExtractHandlers spec)
(map (\x -> (x, Nothing)))
rs
let appData =
AppData variables monitors' copilotM testingAdditionalApps testingVars
variables = mapMaybe (variableMap varDB) varNames
monitors' = mapMaybe (monitorMap varDB) monitors
testingVars
| null testingLimitedVars
= variables
| otherwise
= filter (\x -> varDeclName x `elem` testingLimitedVars) variables
return appData
where
cExpr = commandConditionExpr options
fp = commandInputFile options
varNameFile = commandVariables options
varDBFile = maybeToList $ commandVariableDB options
handlersFile = commandHandlers options
formatName = commandFormat options
propFormatName = commandPropFormat options
propVia = commandPropVia options
parseInputExpr' e =
parseInputExpr e propFormatName propVia exprT
parseInputFile' f =
parseInputFile f formatName propFormatName propVia exprT
processSpec spec' expr' fp' =
Command.Standalone.commandLogic expr' fp' "copilot" [] exprT spec'
testingAdditionalApps = commandTestingApps options
testingLimitedVars = commandTestingVars options
-- ** Argument processing
-- | Options used to customize the conversion of specifications to ROS
-- applications.
data CommandOptions = CommandOptions
{ commandConditionExpr :: Maybe String -- ^ Trigger condition.
, commandInputFile :: Maybe FilePath -- ^ Input specification file.
, commandTargetDir :: FilePath -- ^ Target directory where the
-- application should be created.
, commandTemplateDir :: Maybe FilePath -- ^ Directory where the template is
-- to be found.
, commandVariables :: Maybe FilePath -- ^ File containing a list of
-- variables to make available to
-- Copilot.
, commandVariableDB :: Maybe FilePath -- ^ File containing a list of known
-- variables with their types and the
-- message IDs they can be obtained
-- from.
, commandHandlers :: Maybe FilePath -- ^ File containing a list of
-- handlers used in the Copilot
-- specification. The handlers are
-- assumed to receive no arguments.
, commandFormat :: String -- ^ Format of the input file.
, commandPropFormat :: String -- ^ Format used for input properties.
, commandPropVia :: Maybe String -- ^ Use external command to
-- pre-process system properties.
, commandExtraVars :: Maybe FilePath -- ^ File containing additional
-- variables to make available to the
-- template.
, commandTestingApps :: [Node] -- ^ Additional applications to turn
-- on during testing.
, commandTestingVars :: [String] -- ^ Limited list of variables to use
-- for testing.
}
-- | Return the variable information needed to generate declarations
-- and subscriptions for a given variable name and variable database.
variableMap :: VariableDB
-> String
-> Maybe VarDecl
variableMap varDB varName = do
inputDef <- findInput varDB varName
mid <- connectionTopic <$> findConnection inputDef "ros/message"
topicDef <- findTopic varDB "ros/message" mid
typeVar' <- maybe
(inputType inputDef)
(Just . typeToType)
(findType varDB varName "ros/variable" "C")
let typeMsg' = fromMaybe
(topicType topicDef)
(typeFromType <$> findType varDB varName "ros/message" "C")
return $ VarDecl varName typeVar' mid typeMsg' (randomBaseType typeVar')
-- | Return the monitor information needed to generate declarations and
-- publishers for the given monitor info, and variable database.
monitorMap :: VariableDB
-> (String, Maybe String)
-> Maybe Monitor
monitorMap varDB (monitorName, Nothing) =
Just $ Monitor monitorName Nothing Nothing
monitorMap varDB (monitorName, Just ty) = do
let ty1 = maybe ty typeFromType $ findTypeByType varDB "ros/variable" "C" ty
ty2 <- typeFromType <$> findTypeByType varDB "ros/message" "C" ty
return $ Monitor monitorName (Just ty1) (Just ty2)
-- | The declaration of a variable in C, with a given type and name.
data VarDecl = VarDecl
{ varDeclName :: String
, varDeclType :: String
, varDeclId :: String
, varDeclMsgType :: String
, varDeclRandom :: String
}
deriving Generic
instance ToJSON VarDecl
-- | The name of a handler associated to each condition, and the type
-- of value it receives, together with the type for the message.
data Monitor = Monitor
{ monitorName :: String
, monitorType :: Maybe String
, monitorMsgType :: Maybe String
}
deriving Generic
instance ToJSON Monitor
-- | A package-qualified ROS 2 node name.
data Node = Node
{ nodePackage :: String
, nodeName :: String
}
deriving Generic
instance ToJSON Node
-- | Data that may be relevant to generate a ROS application.
data AppData = AppData
{ variables :: [VarDecl]
, monitors :: [Monitor]
, copilot :: Maybe Command.Standalone.AppData
, testingApps :: [Node]
, testingVariables :: [VarDecl]
}
deriving (Generic)
instance ToJSON AppData
-- | Name of the function to be used to generate random values of a given type.
randomBaseType :: String -- ^ Type to generate random values of.
-> String
randomBaseType ty = case ty of
"bool" -> "randomBool"
"uint8_t" -> "randomInt"
"uint16_t" -> "randomInt"
"uint32_t" -> "randomInt"
"uint64_t" -> "randomInt"
"int8_t" -> "randomInt"
"int16_t" -> "randomInt"
"int32_t" -> "randomInt"
"int64_t" -> "randomInt"
"float" -> "randomFloat"
"double" -> "randomFloat"
def -> def