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