blob: d2475073a05f2ef036a30881646ef87e48c2335e [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2018 CEA LIST.
*
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* Contributors:
* Ansgar Radermacher ansgar.radermacher@cea.fr
*
*****************************************************************************/
package org.eclipse.papyrus.robotics.ros2.codegen.common.utils
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.ArrayList
import java.util.Collections
import java.util.HashMap
import java.util.List
import java.util.Map
import org.eclipse.emf.common.util.UniqueEList
import org.eclipse.papyrus.designer.languages.common.profile.Codegen.NoCodeGen
import org.eclipse.papyrus.designer.languages.cpp.profile.C_Cpp.External
import org.eclipse.papyrus.designer.transformation.base.utils.TransformationException
import org.eclipse.papyrus.robotics.codegen.common.utils.ApplyProfiles
import org.eclipse.papyrus.robotics.core.utils.InteractionUtils
import org.eclipse.papyrus.robotics.core.utils.PortUtils
import org.eclipse.papyrus.robotics.profile.robotics.services.ServiceDefinitionModel
import org.eclipse.papyrus.robotics.ros2.base.ProcessUtils
import org.eclipse.papyrus.robotics.ros2.base.Ros2Constants
import org.eclipse.papyrus.robotics.ros2.base.Ros2ProcessBuilder
import org.eclipse.papyrus.robotics.ros2.codegen.common.Activator
import org.eclipse.papyrus.robotics.ros2.codegen.common.message.CreateMsgPackage
import org.eclipse.papyrus.uml.tools.utils.StereotypeUtil
import org.eclipse.uml2.uml.Class
import org.eclipse.uml2.uml.Classifier
import org.eclipse.uml2.uml.DataType
import org.eclipse.uml2.uml.Element
import org.eclipse.uml2.uml.Interface
import org.eclipse.uml2.uml.NamedElement
import org.eclipse.uml2.uml.Namespace
import org.eclipse.uml2.uml.Package
import org.eclipse.uml2.uml.Port
import org.eclipse.uml2.uml.PrimitiveType
import org.eclipse.uml2.uml.TemplateBinding
import org.eclipse.uml2.uml.TemplateParameterSubstitution
import org.eclipse.uml2.uml.Type
import static extension org.eclipse.papyrus.robotics.codegen.common.utils.ComponentUtils.isRegistered
import static extension org.eclipse.papyrus.robotics.codegen.common.utils.Helpers.escapeCamlCase
import static extension org.eclipse.papyrus.robotics.core.utils.InteractionUtils.*
class MessageUtils {
public val static MESSAGE = "msg"
public val static SERVICE = "srv"
public val static ACTION = "action"
/**
* Return the message package, i.e. the first package (navigating up) that applies the
* ServiceDefinitionModel stereotype
*/
def static getMessagePackage(NamedElement ne) {
if (ne === null || ne.eIsProxy) {
return null
}
var Element pkg = ne.nearestPackage
while (pkg instanceof Package) {
if (StereotypeUtil.isApplied(pkg, ServiceDefinitionModel)) {
return pkg as Package
}
pkg = pkg.owner
}
throw new TransformationException(
String.format("Cannot find service definition module for element %s.", ne.name))
}
/**
* Get the qualified ROS2 message name (i.e. packageName/messageKind/messageName)
* from a data type (communication object).
*/
def static getROS2qMsgName(DataType dt) {
var name = dt.name
val pkg = dt.messagePackage
return String.format("%s/%s/%s", pkg.name.toLowerCase, MESSAGE, name)
}
/**
* Return the qualified ROS2 message name (i.e. packageName/messageKind/messageName)
* from a service definition. It removes the prefix from the service definition
* @param sd a service definition
*/
def static getROS2qMsgName(Interface sd) {
val name = sd.nameWoPrefix
val pattern = sd.templateBinding.communicationPattern
var kind = MESSAGE
if (pattern.isQuery) {
kind = SERVICE
} else if (pattern.isAction) {
kind = ACTION
}
val pkg = sd.messagePackage
return String.format("%s/%s/%s", pkg.name.toLowerCase, kind, name)
}
/**
* Mark a datatype as external (C++ stereotype), since it is used within a message definition and
* ROS will general code for it.
* Instead of its fully qualified name, the type will only be qualified with the ROS2 package
* name
*/
def static void makeDTExternal(DataType dt) {
var external = StereotypeUtil.applyApp(dt, External)
if (external === null) {
ApplyProfiles.applyCppProfile(dt)
external = StereotypeUtil.applyApp(dt, External)
}
val qName = dt.ROS2qMsgName
external.name = qName.replace("/", Namespace.SEPARATOR)
external.incPath = qName.escapeCamlCase + ".hpp"
}
/**
* We should not generate code for the service definition
*/
def static makeSvcDefExternal(TemplateBinding tb) {
val intf = tb.getOwner
StereotypeUtil.applyApp(intf, NoCodeGen)
}
/**
* Transform the service definition into an external ROS2 type.
* This is useful to queries and actions for which the name of the ROS2 service
* or action is used in generated files (not the name of one of the communication
* objects)
*/
def static Interface getServiceType(Port port) {
val tb = port.templateBinding
makeSvcDefExternal(tb)
val sd = tb.serviceDefinition
var external = StereotypeUtil.applyApp(sd, External)
if (external === null) {
ApplyProfiles.applyCppProfile(sd)
external = StereotypeUtil.applyApp(sd, External)
}
// assure that no code (and #include) get generated for the communication pattern
val pattern = tb.communicationPattern
val patternSt = StereotypeUtil.apply(pattern.base_Collaboration, NoCodeGen)
if (patternSt === null) {
ApplyProfiles.applyCommonProfile(pattern.base_Collaboration);
StereotypeUtil.apply(pattern.base_Collaboration, NoCodeGen)
}
val qName = sd.ROS2qMsgName
external.name = qName.replace("/", Namespace.SEPARATOR)
external.incPath = qName.escapeCamlCase + ".hpp"
return sd;
}
/**
* Map a UML primitive type to a native ROS type
*/
def static primitiveTypeMap(Type type) {
// TODO: incomplete
switch type.name {
case "Integer":
"int32"
case "String":
"string"
case "VSL_Expression":
"string" // TODO: more useful mapping
case "Real":
"float64"
case "UnlimitedNatural":
"uint64"
case "Boolean":
"bool"
default:
if (type.name.endsWith("_t") && type.namespace.name.equals("AnsiCLibrary")) {
// handle uintXX_t and others from Ansi-C library
return type.name.substring(0, type.name.length - 2)
} else {
type.name
}
}
}
def static getMsgFileNames(Package msgPackage) {
val msgFileNames = new UniqueEList<String>
for (msg : msgPackage.messages) {
msgFileNames.add(msg.name + ".msg")
}
return msgFileNames
}
def static getSrvFileNames(Package msgPackage) {
val srvFileNames = new UniqueEList<String>
for (sd : msgPackage.queries) {
// use name of service definition, typically consisting of a query and a request
srvFileNames.add(sd.nameWoPrefix + ".srv")
}
return srvFileNames
}
def static getActFileNames(Package msgPackage) {
val actFileNames = new UniqueEList<String>
for (sd : msgPackage.actions) {
// use name of service definition, typically consisting of a query and a request
actFileNames.add(sd.nameWoPrefix + ".action")
}
return actFileNames
}
def static createMessagesOrServices(CreateMsgPackage msgPkgCreator, Class component) {
// val fileAccess = new ProjectBasedFileAccess(TransformationContext.current.project, org.eclipse.papyrus.robotics.ros2.codegen.common.xtend.CreateMessageUtils.MESSAGE)
// TODO take inherited ports into account?
for (port : component.ownedPorts) {
val tb = InteractionUtils.getTemplateBinding(port)
if (tb !== null) {
msgPkgCreator.createMsgPkgs(tb);
}
}
}
/**
* Obtain communication objects for query - request
*/
def static getRequest(TemplateBinding tb) {
return getActual("Request", tb)
}
/**
* Obtain communication objects for query - response
* or action - reponse
*/
def static getResponse(TemplateBinding tb) {
return getActual("Response", tb)
}
/**
* Obtain communication objects for action - goal
*/
def static getGoal(TemplateBinding tb) {
return getActual("Goal", tb)
}
/**
* Obtain communication objects for action - feedback
*/
def static getFeedback(TemplateBinding tb) {
return getActual("Feedback", tb)
}
/**
* Obtain communication objects for query - request
*/
/**
* Obtain an actual from a given formal
*/
def static getActual(String formalName, TemplateBinding tb) {
for (tps : tb.parameterSubstitutions) {
val actual = tps.actual
if (formalName.equals(tps.formal.getTPName) && actual instanceof NamedElement) {
return actual as NamedElement
}
}
return null
}
/**
* Obtain communication objects for push and send, return first
* message object
*/
def static getMessage(TemplateBinding tb) {
for (tps : tb.parameterSubstitutions) {
val actual = tps.actual
if (actual instanceof NamedElement) {
return actual as NamedElement
}
}
return null
}
def static String getFileName(TemplateBinding tb) {
var String names = null;
for (TemplateParameterSubstitution tps : tb.getParameterSubstitutions()) {
val pe = tps.getActual();
if (pe instanceof NamedElement) {
val name = (pe as NamedElement).getName();
// no underscore separator (ROS2 does not support it)
if (names === null) {
names = name;
} else {
names = names + name;
}
}
}
return names;
}
/**
* Convenience method:
* Return the list of package names that are required by a component
* @param the component
*/
def static List<String> calcDependencies(Class component) {
return calcDependencies(Collections.singletonList(component))
}
/**
* Return the list of package names that are required by a list of components
* @param the component list (a unique list in order to avoid duplicates)
*/
def static List<String> calcDependencies(List<Class> components) {
val list = new UniqueEList<String>
list.add("rclcpp")
if (components.isRegistered) {
list.add("rclcpp_components")
}
list.add("rclcpp_lifecycle")
if (components.haveActions) {
list.add("rclcpp_action")
}
// list.add("message_runtime")
for (pkg : calcUsedMessagePackages(components)) {
val pkgName = pkg.name.toLowerCase
if (!list.contains(pkgName)) {
list.add(pkgName)
}
}
return list
}
/**
* Calculate the message-packages referenced by the messages, services
* and actions of a message package
*/
static def calcDependencies(Package msgPackage) {
val commObjects = new UniqueEList<Type>
val transitiveHull = new UniqueEList<Type>
val msgPackages = new UniqueEList<Package>
commObjects.addAll(msgPackage.messages)
for (sd : msgPackage.queries) {
commObjects.addAll(sd.templateBinding.commObjects)
}
for (sd : msgPackage.actions) {
commObjects.addAll(sd.templateBinding.commObjects)
}
for (commObject : commObjects) {
// calculate transitive hull.
allCommObjects(transitiveHull, commObject)
}
for (commObject : transitiveHull) {
if (!(commObject instanceof PrimitiveType)) {
msgPackages.add(commObject.messagePackage)
}
}
// remove dependency to itself
msgPackages.remove(msgPackage)
return msgPackages
}
/**
* Calculate the transitive hull for a given communication object
*/
static def void allCommObjects(List<Type> transitiveHull, Type commObject) {
transitiveHull.add(commObject);
if (commObject instanceof Classifier) {
for (attribute : commObject.attributes) {
if (!transitiveHull.contains(attribute.type)) {
allCommObjects(transitiveHull, attribute.type)
}
}
}
}
/**
* Return the list of packages that are required by a list of components
* @param the component list
*/
def static List<Package> calcUsedMessagePackages(List<Class> components) {
val list = new ArrayList<Package>
for (component : components) {
for (port : PortUtils.getAllPorts(component)) {
val pkg = port.serviceDefinition.messagePackage
if (pkg !== null && !list.contains(pkg)) {
list.add(pkg)
}
}
}
return list
}
/**
* return true, if one of the component port is an action port
*/
def static hasActions(Class component) {
for (port : PortUtils.getAllPorts(component)) {
if (port.communicationPattern.isAction) {
return true
}
}
return false;
}
/**
* return true, if one of the components has action ports
*/
def static haveActions(List<Class> components) {
for (component : components) {
if (component.hasActions) {
return true;
}
}
return false;
}
/**
* Return a hash map of available ROS2 package names
* @return the map containing package names
*/
def static Map<String, Boolean> ros2AvailMsgPkgs() {
val pbMsg = new Ros2ProcessBuilder(Ros2Constants.PKG, Ros2Constants.LIST)
val availPackages = new HashMap<String, Boolean>()
try {
val p = pbMsg.start()
val es = p.getErrorStream()
if (es.read() == -1) {
// no input on error stream => check whether output contains package
val results = new BufferedReader(new InputStreamReader(p.inputStream))
var line = ""
var found = false
while ((line = results.readLine()) !== null && !found) {
availPackages.put(line.trim, true)
}
results.close();
} else {
ProcessUtils.logErrors(p);
}
} catch (IOException e) {
Activator.log.error(e)
}
return availPackages
}
}