at master 9.9 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.klipper; 9 format = pkgs.formats.ini { 10 # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996 11 listToValue = 12 l: 13 if builtins.length l == 1 then 14 lib.generators.mkValueStringDefault { } (lib.head l) 15 else 16 lib.concatMapStrings (s: "\n ${lib.generators.mkValueStringDefault { } s}") l; 17 mkKeyValue = lib.generators.mkKeyValueDefault { } ":"; 18 }; 19in 20{ 21 imports = [ 22 (lib.mkRenamedOptionModule 23 [ "services" "klipper" "mutableConfigFolder" ] 24 [ "services" "klipper" "configDir" ] 25 ) 26 ]; 27 28 ##### interface 29 options = { 30 services.klipper = { 31 enable = lib.mkEnableOption "Klipper, the 3D printer firmware"; 32 33 package = lib.mkPackageOption pkgs "klipper" { }; 34 35 logFile = lib.mkOption { 36 type = lib.types.nullOr lib.types.path; 37 default = null; 38 example = "/var/lib/klipper/klipper.log"; 39 description = '' 40 Path of the file Klipper should log to. 41 If `null`, it logs to stdout, which is not recommended by upstream. 42 ''; 43 }; 44 45 inputTTY = lib.mkOption { 46 type = lib.types.path; 47 default = "/run/klipper/tty"; 48 description = "Path of the virtual printer symlink to create."; 49 }; 50 51 apiSocket = lib.mkOption { 52 type = lib.types.nullOr lib.types.path; 53 default = "/run/klipper/api"; 54 description = "Path of the API socket to create."; 55 }; 56 57 mutableConfig = lib.mkOption { 58 type = lib.types.bool; 59 default = false; 60 example = true; 61 description = '' 62 Whether to manage the config outside of NixOS. 63 64 It will still be initialized with the defined NixOS config if the file doesn't already exist. 65 ''; 66 }; 67 68 configDir = lib.mkOption { 69 type = lib.types.path; 70 default = "/var/lib/klipper"; 71 description = "Path to Klipper config file."; 72 }; 73 74 configFile = lib.mkOption { 75 type = lib.types.nullOr lib.types.path; 76 default = null; 77 description = "Path to default Klipper config."; 78 }; 79 80 octoprintIntegration = lib.mkOption { 81 type = lib.types.bool; 82 default = false; 83 description = "Allows Octoprint to control Klipper."; 84 }; 85 86 user = lib.mkOption { 87 type = lib.types.nullOr lib.types.str; 88 default = null; 89 description = '' 90 User account under which Klipper runs. 91 92 If null is specified (default), a temporary user will be created by systemd. 93 ''; 94 }; 95 96 group = lib.mkOption { 97 type = lib.types.nullOr lib.types.str; 98 default = null; 99 description = '' 100 Group account under which Klipper runs. 101 102 If null is specified (default), a temporary user will be created by systemd. 103 ''; 104 }; 105 106 settings = lib.mkOption { 107 type = lib.types.nullOr format.type; 108 default = null; 109 description = '' 110 Configuration for Klipper. See the [documentation](https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides) 111 for supported values. 112 ''; 113 }; 114 115 extraSettings = lib.mkOption { 116 type = lib.types.lines; 117 default = ""; 118 description = "Extra lines to append to the generated Klipper configuration."; 119 }; 120 121 firmwares = lib.mkOption { 122 description = "Firmwares klipper should manage"; 123 default = { }; 124 type = 125 with lib.types; 126 attrsOf (submodule { 127 options = { 128 enable = lib.mkEnableOption '' 129 building of firmware for manual flashing 130 ''; 131 enableKlipperFlash = lib.mkEnableOption '' 132 flashings scripts for firmware. This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware. 133 Please check the configs at [klipper](https://github.com/Klipper3d/klipper/tree/master/config) whether your board supports flashing via `make flash` 134 ''; 135 serial = lib.mkOption { 136 type = lib.types.nullOr path; 137 default = null; 138 description = "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`."; 139 }; 140 configFile = lib.mkOption { 141 type = path; 142 description = "Path to firmware config which is generated using `klipper-genconf`"; 143 }; 144 }; 145 }); 146 }; 147 }; 148 }; 149 150 ##### implementation 151 config = lib.mkIf cfg.enable { 152 assertions = [ 153 { 154 assertion = cfg.octoprintIntegration -> config.services.octoprint.enable; 155 message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it."; 156 } 157 { 158 assertion = cfg.user != null -> cfg.group != null; 159 message = "Option services.klipper.group is not set when services.klipper.user is specified."; 160 } 161 { 162 assertion = 163 cfg.settings != null 164 -> lib.foldl (a: b: a && b) true ( 165 lib.mapAttrsToList ( 166 mcu: _: mcu != null -> (lib.hasAttrByPath [ "${mcu}" "serial" ] cfg.settings) 167 ) cfg.firmwares 168 ); 169 message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified"; 170 } 171 { 172 assertion = (cfg.configFile != null) != (cfg.settings != null); 173 message = "You need to either specify services.klipper.settings or services.klipper.configFile."; 174 } 175 { 176 assertion = (cfg.configFile != null) -> (cfg.extraSettings == ""); 177 message = "You can't use services.klipper.extraSettings with services.klipper.configFile."; 178 } 179 ]; 180 181 services.klipper = lib.mkIf cfg.octoprintIntegration { 182 user = config.services.octoprint.user; 183 group = config.services.octoprint.group; 184 }; 185 186 systemd.services.klipper = 187 let 188 klippyArgs = 189 "--input-tty=${cfg.inputTTY}" 190 + lib.optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}" 191 + lib.optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}"; 192 printerConfig = 193 if cfg.settings != null then 194 builtins.toFile "klipper.cfg" ((format.generate "" cfg.settings).text + cfg.extraSettings) 195 else 196 cfg.configFile; 197 in 198 { 199 description = "Klipper 3D Printer Firmware"; 200 wantedBy = [ "multi-user.target" ]; 201 after = [ "network.target" ]; 202 preStart = '' 203 mkdir -p ${cfg.configDir} 204 pushd ${cfg.configDir} 205 if [ -e printer.cfg ]; then 206 ${ 207 if cfg.mutableConfig then 208 ":" 209 else 210 '' 211 # Backup existing config using the same date format klipper uses for SAVE_CONFIG 212 old_config="printer-$(date +"%Y%m%d_%H%M%S").cfg" 213 mv printer.cfg "$old_config" 214 # Preserve SAVE_CONFIG section from the existing config 215 cat ${printerConfig} <(printf "\n") <(sed -n '/#*# <---------------------- SAVE_CONFIG ---------------------->/,$p' "$old_config") > printer.cfg 216 ${pkgs.diffutils}/bin/cmp printer.cfg "$old_config" && rm "$old_config" 217 '' 218 } 219 else 220 cat ${printerConfig} > printer.cfg 221 fi 222 popd 223 ''; 224 225 restartTriggers = lib.optional (!cfg.mutableConfig) [ printerConfig ]; 226 227 serviceConfig = { 228 ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${cfg.configDir}/printer.cfg"; 229 RuntimeDirectory = "klipper"; 230 StateDirectory = "klipper"; 231 SupplementaryGroups = [ "dialout" ]; 232 WorkingDirectory = "${cfg.package}/lib"; 233 OOMScoreAdjust = "-999"; 234 CPUSchedulingPolicy = "rr"; 235 CPUSchedulingPriority = 99; 236 IOSchedulingClass = "realtime"; 237 IOSchedulingPriority = 0; 238 UMask = "0002"; 239 } 240 // ( 241 if cfg.user != null then 242 { 243 Group = cfg.group; 244 User = cfg.user; 245 } 246 else 247 { 248 DynamicUser = true; 249 User = "klipper"; 250 } 251 ); 252 }; 253 254 environment.systemPackages = 255 let 256 default = a: b: if a != null then a else b; 257 genconf = pkgs.klipper-genconf.override { 258 klipper = cfg.package; 259 }; 260 firmwares = lib.filterAttrs (n: v: v != null) ( 261 lib.mapAttrs ( 262 mcu: 263 { 264 enable, 265 enableKlipperFlash, 266 configFile, 267 serial, 268 }: 269 if enable then 270 pkgs.klipper-firmware.override { 271 klipper = cfg.package; 272 mcu = lib.strings.sanitizeDerivationName mcu; 273 firmwareConfig = configFile; 274 } 275 else 276 null 277 ) cfg.firmwares 278 ); 279 firmwareFlasher = lib.mapAttrsToList ( 280 mcu: firmware: 281 pkgs.klipper-flash.override { 282 klipper = cfg.package; 283 klipper-firmware = firmware; 284 mcu = lib.strings.sanitizeDerivationName mcu; 285 flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial; 286 firmwareConfig = cfg.firmwares."${mcu}".configFile; 287 } 288 ) (lib.filterAttrs (mcu: firmware: cfg.firmwares."${mcu}".enableKlipperFlash) firmwares); 289 in 290 [ genconf ] ++ firmwareFlasher ++ lib.attrValues firmwares; 291 }; 292 meta.maintainers = [ 293 lib.maintainers.cab404 294 ]; 295}