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}