at 23.11-pre 8.4 kB view raw
1{ config, lib, pkgs, ... }: 2with lib; 3let 4 cfg = config.services.klipper; 5 format = pkgs.formats.ini { 6 # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996 7 listToValue = l: 8 if builtins.length l == 1 then generators.mkValueStringDefault { } (head l) 9 else lib.concatMapStrings (s: "\n ${generators.mkValueStringDefault {} s}") l; 10 mkKeyValue = generators.mkKeyValueDefault { } ":"; 11 }; 12in 13{ 14 ##### interface 15 options = { 16 services.klipper = { 17 enable = mkEnableOption (lib.mdDoc "Klipper, the 3D printer firmware"); 18 19 package = mkOption { 20 type = types.package; 21 default = pkgs.klipper; 22 defaultText = literalExpression "pkgs.klipper"; 23 description = lib.mdDoc "The Klipper package."; 24 }; 25 26 logFile = mkOption { 27 type = types.nullOr types.path; 28 default = null; 29 example = "/var/lib/klipper/klipper.log"; 30 description = lib.mdDoc '' 31 Path of the file Klipper should log to. 32 If `null`, it logs to stdout, which is not recommended by upstream. 33 ''; 34 }; 35 36 inputTTY = mkOption { 37 type = types.path; 38 default = "/run/klipper/tty"; 39 description = lib.mdDoc "Path of the virtual printer symlink to create."; 40 }; 41 42 apiSocket = mkOption { 43 type = types.nullOr types.path; 44 default = "/run/klipper/api"; 45 description = lib.mdDoc "Path of the API socket to create."; 46 }; 47 48 mutableConfig = mkOption { 49 type = types.bool; 50 default = false; 51 example = true; 52 description = lib.mdDoc '' 53 Whether to copy the config to a mutable directory instead of using the one directly from the nix store. 54 This will only copy the config if the file at `services.klipper.mutableConfigPath` doesn't exist. 55 ''; 56 }; 57 58 mutableConfigFolder = mkOption { 59 type = types.path; 60 default = "/var/lib/klipper"; 61 description = lib.mdDoc "Path to mutable Klipper config file."; 62 }; 63 64 configFile = mkOption { 65 type = types.nullOr types.path; 66 default = null; 67 description = lib.mdDoc '' 68 Path to default Klipper config. 69 ''; 70 }; 71 72 octoprintIntegration = mkOption { 73 type = types.bool; 74 default = false; 75 description = lib.mdDoc "Allows Octoprint to control Klipper."; 76 }; 77 78 user = mkOption { 79 type = types.nullOr types.str; 80 default = null; 81 description = lib.mdDoc '' 82 User account under which Klipper runs. 83 84 If null is specified (default), a temporary user will be created by systemd. 85 ''; 86 }; 87 88 group = mkOption { 89 type = types.nullOr types.str; 90 default = null; 91 description = lib.mdDoc '' 92 Group account under which Klipper runs. 93 94 If null is specified (default), a temporary user will be created by systemd. 95 ''; 96 }; 97 98 settings = mkOption { 99 type = types.nullOr format.type; 100 default = null; 101 description = lib.mdDoc '' 102 Configuration for Klipper. See the [documentation](https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides) 103 for supported values. 104 ''; 105 }; 106 107 firmwares = mkOption { 108 description = lib.mdDoc "Firmwares klipper should manage"; 109 default = { }; 110 type = with types; attrsOf 111 (submodule { 112 options = { 113 enable = mkEnableOption (lib.mdDoc '' 114 building of firmware and addition of klipper-flash tools for manual flashing. 115 This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware. 116 ''); 117 serial = mkOption { 118 type = types.nullOr path; 119 description = lib.mdDoc "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`."; 120 }; 121 configFile = mkOption { 122 type = path; 123 description = lib.mdDoc "Path to firmware config which is generated using `klipper-genconf`"; 124 }; 125 }; 126 }); 127 }; 128 }; 129 }; 130 131 ##### implementation 132 config = mkIf cfg.enable { 133 assertions = [ 134 { 135 assertion = cfg.octoprintIntegration -> config.services.octoprint.enable; 136 message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it."; 137 } 138 { 139 assertion = cfg.user != null -> cfg.group != null; 140 message = "Option services.klipper.group is not set when services.klipper.user is specified."; 141 } 142 { 143 assertion = cfg.settings != null -> foldl (a: b: a && b) true (mapAttrsToList (mcu: _: mcu != null -> (hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)) cfg.firmwares); 144 message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified"; 145 } 146 { 147 assertion = (cfg.configFile != null) != (cfg.settings != null); 148 message = "You need to either specify services.klipper.settings or services.klipper.configFile."; 149 } 150 ]; 151 152 environment.etc = mkIf (!cfg.mutableConfig) { 153 "klipper.cfg".source = if cfg.settings != null then format.generate "klipper.cfg" cfg.settings else cfg.configFile; 154 }; 155 156 services.klipper = mkIf cfg.octoprintIntegration { 157 user = config.services.octoprint.user; 158 group = config.services.octoprint.group; 159 }; 160 161 systemd.services.klipper = 162 let 163 klippyArgs = "--input-tty=${cfg.inputTTY}" 164 + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}" 165 + optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}" 166 ; 167 printerConfigPath = 168 if cfg.mutableConfig 169 then cfg.mutableConfigFolder + "/printer.cfg" 170 else "/etc/klipper.cfg"; 171 printerConfigFile = 172 if cfg.settings != null 173 then format.generate "klipper.cfg" cfg.settings 174 else cfg.configFile; 175 in 176 { 177 description = "Klipper 3D Printer Firmware"; 178 wantedBy = [ "multi-user.target" ]; 179 after = [ "network.target" ]; 180 preStart = '' 181 mkdir -p ${cfg.mutableConfigFolder} 182 ${lib.optionalString (cfg.mutableConfig) '' 183 [ -e ${printerConfigPath} ] || { 184 cp ${printerConfigFile} ${printerConfigPath} 185 chmod +w ${printerConfigPath} 186 } 187 ''} 188 mkdir -p ${cfg.mutableConfigFolder}/gcodes 189 ''; 190 191 serviceConfig = { 192 ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${printerConfigPath}"; 193 RuntimeDirectory = "klipper"; 194 StateDirectory = "klipper"; 195 SupplementaryGroups = [ "dialout" ]; 196 WorkingDirectory = "${cfg.package}/lib"; 197 OOMScoreAdjust = "-999"; 198 CPUSchedulingPolicy = "rr"; 199 CPUSchedulingPriority = 99; 200 IOSchedulingClass = "realtime"; 201 IOSchedulingPriority = 0; 202 UMask = "0002"; 203 } // (if cfg.user != null then { 204 Group = cfg.group; 205 User = cfg.user; 206 } else { 207 DynamicUser = true; 208 User = "klipper"; 209 }); 210 }; 211 212 environment.systemPackages = 213 with pkgs; 214 let 215 default = a: b: if a != null then a else b; 216 firmwares = filterAttrs (n: v: v!= null) (mapAttrs 217 (mcu: { enable, configFile, serial }: if enable then pkgs.klipper-firmware.override { 218 mcu = lib.strings.sanitizeDerivationName mcu; 219 firmwareConfig = configFile; 220 } else null) 221 cfg.firmwares); 222 firmwareFlasher = mapAttrsToList 223 (mcu: firmware: pkgs.klipper-flash.override { 224 mcu = lib.strings.sanitizeDerivationName mcu; 225 klipper-firmware = firmware; 226 flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial; 227 firmwareConfig = cfg.firmwares."${mcu}".configFile; 228 }) 229 firmwares; 230 in 231 [ klipper-genconf ] ++ firmwareFlasher ++ attrValues firmwares; 232 }; 233 meta.maintainers = [ 234 maintainers.cab404 235 ]; 236}