From 6c39cd534e828ce2cf38ff81b565caa2fc72af67 Mon Sep 17 00:00:00 2001 From: Ben Wolsieffer Date: Mon, 7 Nov 2022 13:03:33 -0500 Subject: [PATCH] Add basic ROS2 support to the NixOS modules. --- modules/common.nix | 27 ++++++ modules/default.nix | 9 +- modules/{ => ros1}/core.nix | 0 modules/{ => ros1}/nodes.nix | 0 modules/{ => ros1}/ros.nix | 15 +-- modules/ros2/nodes.nix | 172 +++++++++++++++++++++++++++++++++++ modules/ros2/ros.nix | 89 ++++++++++++++++++ 7 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 modules/common.nix rename modules/{ => ros1}/core.nix (100%) rename modules/{ => ros1}/nodes.nix (100%) rename modules/{ => ros1}/ros.nix (84%) create mode 100644 modules/ros2/nodes.nix create mode 100644 modules/ros2/ros.nix diff --git a/modules/common.nix b/modules/common.nix new file mode 100644 index 0000000000..a4f3bff6ce --- /dev/null +++ b/modules/common.nix @@ -0,0 +1,27 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + ros1Cfg = config.services.ros; + ros2Cfg = config.services.ros2; +in { + # Interface + + # Implementation + + config = mkIf (ros1Cfg.enable || ros2Cfg.enable) { + environment.etc."ros/rosdep/sources.list.d/20-default.list".source = pkgs.fetchurl { + url = "https://raw.githubusercontent.com/ros/rosdistro/225c14be89fdf7ecf028b4cf85fa82032f7728e1/rosdep/sources.list.d/20-default.list"; + sha256 = "0kxknc42y01pci8fxzhg84ybhgqyxqimycck27vb4b282lqfkzj7"; + }; + + users = { + users.ros = { + group = "ros"; + isSystemUser = true; + }; + groups.ros = { }; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix index 938104592e..9feb559ed4 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,7 +1,10 @@ { ... }: { imports = [ - ./core.nix - ./ros.nix - ./nodes.nix + ./common.nix + ./ros1/core.nix + ./ros1/ros.nix + ./ros1/nodes.nix + ./ros2/ros.nix + ./ros2/nodes.nix ]; } diff --git a/modules/core.nix b/modules/ros1/core.nix similarity index 100% rename from modules/core.nix rename to modules/ros1/core.nix diff --git a/modules/nodes.nix b/modules/ros1/nodes.nix similarity index 100% rename from modules/nodes.nix rename to modules/ros1/nodes.nix diff --git a/modules/ros.nix b/modules/ros1/ros.nix similarity index 84% rename from modules/ros.nix rename to modules/ros1/ros.nix index 66d2e3e116..2d5971986f 100644 --- a/modules/ros.nix +++ b/modules/ros1/ros.nix @@ -80,7 +80,7 @@ in { # FIXME: mkAfter is used to make sure the Python overlay is applied. That # means all other user configured Python overlays are ignored. This needs a # fix in nixpkgs: https://github.com/NixOS/nixpkgs/issues/44426 - nixpkgs.overlays = mkAfter (singleton (import ../overlay.nix)); + nixpkgs.overlays = mkAfter (singleton (import ../../overlay.nix)); services.ros = { pkgs = mkDefault (pkgs.rosPackages."${cfg.distro}".overrideScope cfg.overlays); @@ -89,11 +89,6 @@ in { masterUri = mkDefault "http://${cfg.hostname}:11311/"; }; - environment.etc."ros/rosdep/sources.list.d/20-default.list".source = pkgs.fetchurl { - url = "https://raw.githubusercontent.com/ros/rosdistro/225c14be89fdf7ecf028b4cf85fa82032f7728e1/rosdep/sources.list.d/20-default.list"; - sha256 = "0kxknc42y01pci8fxzhg84ybhgqyxqimycck27vb4b282lqfkzj7"; - }; - environment.variables = { ROS_HOSTNAME = cfg.hostname; ROS_MASTER_URI = cfg.masterUri; @@ -106,13 +101,5 @@ in { inherit paths; extraOutputsToInstall = optional config.environment.enableDebugInfo "debug"; }) ]; - - users = { - users.ros = { - group = "ros"; - isSystemUser = true; - }; - groups.ros = { }; - }; }; } diff --git a/modules/ros2/nodes.nix b/modules/ros2/nodes.nix new file mode 100644 index 0000000000..6177ca3f8c --- /dev/null +++ b/modules/ros2/nodes.nix @@ -0,0 +1,172 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.ros2; + + commonServiceOptions = { + package = mkOption { + type = types.str; + description = '' + ROS package name containing this node or launch file. This is the ROS + name for the package, rather than the attribute name, meaning it uses + underscores rather than dashes. + ''; + }; + + paths = mkOption { + type = types.listOf types.package; + description = '' + Additional paths to add to the environment of this node. The + option will be turned into an attribute + name and automatically added to this option if valid. + ''; + }; + + env = mkOption { + type = types.package; + description = '' + Environment created with the ROS specific buildEnv function for + this node. + ''; + }; + }; + + commonServiceConfig = { name, config, ... }: { + # Try to convert package name to attribute and add it to the + # environment + paths = let + packageAttr = replaceStrings ["_"] ["-"] config.package; + in optional (hasAttr packageAttr cfg.pkgs) cfg.pkgs."${packageAttr}"; + + env = mkDefault (cfg.pkgs.buildEnv { + name = "ros-node-${name}-env"; + inherit (config) paths; + }); + }; + + serviceGenerator = execStartFn: services: mapAttrs' (name: config: nameValuePair name { + serviceConfig = { + Type = "exec"; + StateDirectory = "ros"; + User = "ros"; + Group = "ros"; + ExecStart = execStartFn config; + }; + wantedBy = [ "multi-user.target" ]; + environment = { + ROS_DOMAIN_ID = toString cfg.domainId; + ROS_HOME = "/var/lib/ros"; + }; + }) services; +in { + # Interface + + options.services.ros2 = { + nodes = mkOption { + type = types.attrsOf (types.submodule ({ name, config, ... }@args: { + options = commonServiceOptions // { + node = mkOption { + type = types.str; + description = '' + Name of the node to launch. + ''; + }; + + args = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Arguments to pass to the node. + ''; + }; + + rosArgs = mkOption { + type = types.listOf types.str; + default = []; + description = '' + ROS specific arguments to pass to the node using --ros-args + ''; + }; + + params = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Key-value parameters to pass to the node. + ''; + }; + }; + + config = mkMerge [ + (commonServiceConfig args) + { + paths = [ cfg.pkgs.ros2run ]; + rosArgs = concatMap (k: v: [ "--param" "${k}:=${v}" ]) cfg.params; + } + ]; + })); + default = {}; + description = '' + ROS nodes to launch at boot as systemd services. + ''; + }; + + launchFiles = mkOption { + type = types.attrsOf (types.submodule ({ name, config, ... }@args: { + options = commonServiceOptions // { + launchFile = mkOption { + type = types.str; + description = '' + Name of the launch file. + ''; + }; + + args = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Key-value arguments to pass to the launch file + ''; + }; + + launchArgs = mkOption { + type = types.listOf types.str; + default = []; + description = '' + Extra command line arguments to pass to ros2 launch. + ''; + }; + }; + + config = mkMerge [ + (commonServiceConfig args) + { paths = with cfg.pkgs; [ cfg.pkgs.ros2launch ]; } + ]; + })); + default = {}; + description = '' + ROS launch files to start at boot as systemd services. + ''; + }; + }; + + # Implementation + + config = mkIf cfg.enable { + systemd.services = mkMerge [ + (serviceGenerator (config: escapeShellArgs ( + [ "${config.env}/bin/ros2" "run" config.package config.node ] ++ + config.args ++ + [ "--ros-args" ] ++ config.rosArgs ++ [ "--" ] + )) cfg.nodes) + (serviceGenerator (config: escapeShellArgs ( + [ "${config.env}/bin/ros2" "launch" ] ++ + config.launchArgs ++ + [ config.package config.launchFile ] ++ + (mapAttrsToList (n: v: "${n}:=${v}") config.args) + )) cfg.launchFiles) + ]; + }; +} diff --git a/modules/ros2/ros.nix b/modules/ros2/ros.nix new file mode 100644 index 0000000000..80e5782d64 --- /dev/null +++ b/modules/ros2/ros.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.ros2; + + pkgsType = mkOptionType { + name = "ros-packages"; + description = "ROS package set"; + check = p: isAttrs p && hasAttr "ament-cmake" p; + }; + overlayType = mkOptionType { + name = "ros-overlay"; + description = "ROS package set overlay"; + check = isFunction; + }; +in { + # Interface + + options.services.ros2 = { + enable = mkEnableOption "Robot Operating System, version 2"; + + distro = mkOption { + type = types.str; + default = "humble"; + description = '' + ROS distro to use. Must be defined in distros/default.nix. + ''; + }; + + pkgs = mkOption { + type = pkgsType; + description = '' + ROS package set for the selected distro. + ''; + }; + + overlays = mkOption { + type = types.listOf overlayType; + default = []; + apply = composeManyExtensions; + description = '' + Set of package overlays to apply to ROS package set for the configured + distro. + ''; + }; + + domainId = mkOption { + type = types.ints.between 0 232; + default = 0; + description = '' + DDS Domain ID that defines the ports used for communication between + processes. + ''; + }; + + systemPackages = mkOption { + default = p: []; + example = literalExample "p: with p; [ ros2cli ros2run ]"; + description = '' + Packages to add to a ROS environment that will be added to the system + PATH. The provided function will be passed the package set configured by + . + ''; + }; + }; + + # Implementation + + config = mkIf cfg.enable { + # FIXME: mkAfter is used to make sure the Python overlay is applied. That + # means all other user configured Python overlays are ignored. This needs a + # fix in nixpkgs: https://github.com/NixOS/nixpkgs/issues/44426 + nixpkgs.overlays = mkAfter (singleton (import ../../overlay.nix)); + + services.ros2.pkgs = mkDefault (pkgs.rosPackages."${cfg.distro}".overrideScope cfg.overlays); + + environment.variables.ROS_DOMAIN_ID = toString cfg.domainId; + + environment.systemPackages = let + paths = cfg.systemPackages cfg.pkgs; + in mkIf (length paths != 0) [ (cfg.pkgs.buildEnv { + name = "ros2-system-env"; + inherit paths; + extraOutputsToInstall = optional config.environment.enableDebugInfo "debug"; + }) ]; + }; +}