Add basic ROS2 support to the NixOS modules.

This commit is contained in:
Ben Wolsieffer 2022-11-07 13:03:33 -05:00
parent c0077930c0
commit 6c39cd534e
7 changed files with 295 additions and 17 deletions

27
modules/common.nix Normal file
View file

@ -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 = { };
};
};
}

View file

@ -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
];
}

View file

@ -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 = { };
};
};
}

172
modules/ros2/nodes.nix Normal file
View file

@ -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>package</option> 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)
];
};
}

89
modules/ros2/ros.nix Normal file
View file

@ -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
<option>services.ros.pkgs</option>.
'';
};
};
# 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";
}) ];
};
}