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;
+ };
+}