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 firmwares = lib.mkOption {
116 description = "Firmwares klipper should manage";
117 default = { };
118 type =
119 with lib.types;
120 attrsOf (submodule {
121 options = {
122 enable = lib.mkEnableOption ''
123 building of firmware for manual flashing
124 '';
125 enableKlipperFlash = lib.mkEnableOption ''
126 flashings scripts for firmware. This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware.
127 Please check the configs at [klipper](https://github.com/Klipper3d/klipper/tree/master/config) whether your board supports flashing via `make flash`
128 '';
129 serial = lib.mkOption {
130 type = lib.types.nullOr path;
131 default = null;
132 description = "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`.";
133 };
134 configFile = lib.mkOption {
135 type = path;
136 description = "Path to firmware config which is generated using `klipper-genconf`";
137 };
138 };
139 });
140 };
141 };
142 };
143
144 ##### implementation
145 config = lib.mkIf cfg.enable {
146 assertions = [
147 {
148 assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
149 message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
150 }
151 {
152 assertion = cfg.user != null -> cfg.group != null;
153 message = "Option services.klipper.group is not set when services.klipper.user is specified.";
154 }
155 {
156 assertion =
157 cfg.settings != null
158 -> lib.foldl (a: b: a && b) true (
159 lib.mapAttrsToList (
160 mcu: _: mcu != null -> (lib.hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)
161 ) cfg.firmwares
162 );
163 message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified";
164 }
165 {
166 assertion = (cfg.configFile != null) != (cfg.settings != null);
167 message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
168 }
169 ];
170
171 services.klipper = lib.mkIf cfg.octoprintIntegration {
172 user = config.services.octoprint.user;
173 group = config.services.octoprint.group;
174 };
175
176 systemd.services.klipper =
177 let
178 klippyArgs =
179 "--input-tty=${cfg.inputTTY}"
180 + lib.optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}"
181 + lib.optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}";
182 printerConfig =
183 if cfg.settings != null then format.generate "klipper.cfg" cfg.settings else cfg.configFile;
184 in
185 {
186 description = "Klipper 3D Printer Firmware";
187 wantedBy = [ "multi-user.target" ];
188 after = [ "network.target" ];
189 preStart = ''
190 mkdir -p ${cfg.configDir}
191 pushd ${cfg.configDir}
192 if [ -e printer.cfg ]; then
193 ${
194 if cfg.mutableConfig then
195 ":"
196 else
197 ''
198 # Backup existing config using the same date format klipper uses for SAVE_CONFIG
199 old_config="printer-$(date +"%Y%m%d_%H%M%S").cfg"
200 mv printer.cfg "$old_config"
201 # Preserve SAVE_CONFIG section from the existing config
202 cat ${printerConfig} <(printf "\n") <(sed -n '/#*# <---------------------- SAVE_CONFIG ---------------------->/,$p' "$old_config") > printer.cfg
203 ${pkgs.diffutils}/bin/cmp printer.cfg "$old_config" && rm "$old_config"
204 ''
205 }
206 else
207 cat ${printerConfig} > printer.cfg
208 fi
209 popd
210 '';
211
212 restartTriggers = lib.optional (!cfg.mutableConfig) [ printerConfig ];
213
214 serviceConfig =
215 {
216 ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${cfg.configDir}/printer.cfg";
217 RuntimeDirectory = "klipper";
218 StateDirectory = "klipper";
219 SupplementaryGroups = [ "dialout" ];
220 WorkingDirectory = "${cfg.package}/lib";
221 OOMScoreAdjust = "-999";
222 CPUSchedulingPolicy = "rr";
223 CPUSchedulingPriority = 99;
224 IOSchedulingClass = "realtime";
225 IOSchedulingPriority = 0;
226 UMask = "0002";
227 }
228 // (
229 if cfg.user != null then
230 {
231 Group = cfg.group;
232 User = cfg.user;
233 }
234 else
235 {
236 DynamicUser = true;
237 User = "klipper";
238 }
239 );
240 };
241
242 environment.systemPackages =
243 let
244 default = a: b: if a != null then a else b;
245 genconf = pkgs.klipper-genconf.override {
246 klipper = cfg.package;
247 };
248 firmwares = lib.filterAttrs (n: v: v != null) (
249 lib.mapAttrs (
250 mcu:
251 {
252 enable,
253 enableKlipperFlash,
254 configFile,
255 serial,
256 }:
257 if enable then
258 pkgs.klipper-firmware.override {
259 klipper = cfg.package;
260 mcu = lib.strings.sanitizeDerivationName mcu;
261 firmwareConfig = configFile;
262 }
263 else
264 null
265 ) cfg.firmwares
266 );
267 firmwareFlasher = lib.mapAttrsToList (
268 mcu: firmware:
269 pkgs.klipper-flash.override {
270 klipper = cfg.package;
271 klipper-firmware = firmware;
272 mcu = lib.strings.sanitizeDerivationName mcu;
273 flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial;
274 firmwareConfig = cfg.firmwares."${mcu}".configFile;
275 }
276 ) (lib.filterAttrs (mcu: firmware: cfg.firmwares."${mcu}".enableKlipperFlash) firmwares);
277 in
278 [ genconf ] ++ firmwareFlasher ++ lib.attrValues firmwares;
279 };
280 meta.maintainers = [
281 lib.maintainers.cab404
282 ];
283}