diff --git a/modules/core.nix b/modules/core.nix new file mode 100644 index 0000000000..ee0ce4a40f --- /dev/null +++ b/modules/core.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + rosCfg = config.services.ros; + cfg = rosCfg.core; +in { + # Interface + + options.services.ros.core = { + enable = mkEnableOption "roscore"; + + port = mkOption { + type = types.ints.unsigned; + default = 11311; + description = '' + Port the ROS master will bind to. + ''; + }; + }; + + # Implementation + + config = mkIf cfg.enable { + systemd.services.roscore = { + description = "ROS core"; + serviceConfig = { + Type = "exec"; + ExecStart = let + env = with rosCfg.pkgs; buildEnv { + name = "roscore-env"; + paths = [ roslaunch ]; + }; + in "${env}/bin/roscore -p ${toString cfg.port}"; + User = "ros"; + Group = "ros"; + StateDirectory = "ros"; + }; + wantedBy = [ "multi-user.target" ]; + environment = { + ROS_HOSTNAME = rosCfg.hostname; + ROS_MASTER_URI = rosCfg.masterUri; + ROS_HOME = "/var/lib/ros"; + }; + }; + }; +} diff --git a/modules/default.nix b/modules/default.nix new file mode 100644 index 0000000000..af7217b7f9 --- /dev/null +++ b/modules/default.nix @@ -0,0 +1,6 @@ +{ ... }: { + imports = [ + ./ros.nix + ./core.nix + ]; +} diff --git a/modules/ros.nix b/modules/ros.nix new file mode 100644 index 0000000000..7468468c3d --- /dev/null +++ b/modules/ros.nix @@ -0,0 +1,175 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.ros; + + pkgsType = mkOptionType { + name = "ros-packages"; + description = "ROS package set"; + check = p: isAttrs p && hasAttr "roslaunch" p; + }; + overlayType = mkOptionType { + name = "ros-overlay"; + description = "ROS package set overlay"; + check = isFunction; + }; +in { + # Interface + + options.services.ros = { + enable = mkEnableOption "Robot Operating System"; + + distro = mkOption { + type = types.str; + default = "noetic"; + 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 = foldr composeExtensions (_: _: {}); + description = '' + Set of package overlays to apply to ROS package set for the configured + distro. + ''; + }; + + hostname = mkOption { + type = types.str; + example = "localhost"; + description = '' + Value of the ROS_HOSTNAME environment variable. Defaults to + . + ''; + }; + + masterUri = mkOption { + type = types.str; + example = "https://localhost:11311/"; + description = '' + Value of the ROS_MASTER_URI environment variable. + ''; + }; + + nodes = mkOption { + type = types.attrsOf (types.submodule ({ name, config, ... }: { + options = { + package = mkOption { + type = types.str; + description = '' + ROS package name containing this node. This is the ROS name for + the package, rather than the attribute name, meaning it uses + underscores rather than dashes. + ''; + }; + + 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. + ''; + }; + + 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. + ''; + }; + }; + + config = { + # Try to convert package name to attribute and add it to the + # environment + paths = [ cfg.pkgs.rosbash ] ++ (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; + }); + }; + })); + default = {}; + description = '' + ROS nodes to launch at boot as systemd services. + ''; + }; + }; + + # 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.ros = { + pkgs = mkDefault (pkgs.rosPackages."${cfg.distro}".extend cfg.overlays); + + hostname = mkDefault config.networking.hostName; + masterUri = mkDefault "http://${cfg.hostname}:11311/"; + }; + + environment.variables = { + ROS_HOSTNAME = cfg.hostname; + ROS_MASTER_URI = cfg.masterUri; + }; + + users = { + users.ros = { + group = "ros"; + isSystemUser = true; + }; + groups.ros = { }; + }; + + systemd.services = mapAttrs' (name: config: nameValuePair name { + serviceConfig = { + Type = "exec"; + StateDirectory = "ros"; + User = "ros"; + Group = "ros"; + ExecStart = escapeShellArgs ([ "${config.env}/bin/rosrun" config.package config.node ] ++ config.args); + }; + wantedBy = [ "multi-user.target" ]; + environment = { + ROS_HOSTNAME = cfg.hostname; + ROS_MASTER_URI = cfg.masterUri; + ROS_HOME = "/var/lib/ros"; + }; + }) cfg.nodes; + }; +}