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}