about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/misc/klipper.nix
blob: a0eb409599b595efa4aaf71f5e415a47b2202e9c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
{ config, lib, pkgs, ... }:
with lib;
let
  cfg = config.services.klipper;
  format = pkgs.formats.ini {
    # https://github.com/NixOS/nixpkgs/pull/121613#issuecomment-885241996
    listToValue = l:
      if builtins.length l == 1 then generators.mkValueStringDefault { } (head l)
      else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
    mkKeyValue = generators.mkKeyValueDefault { } ":";
  };
in
{
  ##### interface
  options = {
    services.klipper = {
      enable = mkEnableOption (lib.mdDoc "Klipper, the 3D printer firmware");

      package = mkPackageOption pkgs "klipper" { };

      logFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        example = "/var/lib/klipper/klipper.log";
        description = lib.mdDoc ''
          Path of the file Klipper should log to.
          If `null`, it logs to stdout, which is not recommended by upstream.
        '';
      };

      inputTTY = mkOption {
        type = types.path;
        default = "/run/klipper/tty";
        description = lib.mdDoc "Path of the virtual printer symlink to create.";
      };

      apiSocket = mkOption {
        type = types.nullOr types.path;
        default = "/run/klipper/api";
        description = lib.mdDoc "Path of the API socket to create.";
      };

      mutableConfig = mkOption {
        type = types.bool;
        default = false;
        example = true;
        description = lib.mdDoc ''
          Whether to copy the config to a mutable directory instead of using the one directly from the nix store.
          This will only copy the config if the file at `services.klipper.mutableConfigPath` doesn't exist.
        '';
      };

      mutableConfigFolder = mkOption {
        type = types.path;
        default = "/var/lib/klipper";
        description = lib.mdDoc "Path to mutable Klipper config file.";
      };

      configFile = mkOption {
        type = types.nullOr types.path;
        default = null;
        description = lib.mdDoc ''
          Path to default Klipper config.
        '';
      };

      octoprintIntegration = mkOption {
        type = types.bool;
        default = false;
        description = lib.mdDoc "Allows Octoprint to control Klipper.";
      };

      user = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = lib.mdDoc ''
          User account under which Klipper runs.

          If null is specified (default), a temporary user will be created by systemd.
        '';
      };

      group = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = lib.mdDoc ''
          Group account under which Klipper runs.

          If null is specified (default), a temporary user will be created by systemd.
        '';
      };

      settings = mkOption {
        type = types.nullOr format.type;
        default = null;
        description = lib.mdDoc ''
          Configuration for Klipper. See the [documentation](https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides)
          for supported values.
        '';
      };

      firmwares = mkOption {
        description = lib.mdDoc "Firmwares klipper should manage";
        default = { };
        type = with types; attrsOf
          (submodule {
            options = {
              enable = mkEnableOption (lib.mdDoc ''
                building of firmware for manual flashing
              '');
              enableKlipperFlash = mkEnableOption (lib.mdDoc ''
                flashings scripts for firmware. This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware.
                Please check the configs at [klipper](https://github.com/Klipper3d/klipper/tree/master/config) whether your board supports flashing via `make flash`
              '');
              serial = mkOption {
                type = types.nullOr path;
                description = lib.mdDoc "Path to serial port this printer is connected to. Leave `null` to derive it from `service.klipper.settings`.";
              };
              configFile = mkOption {
                type = path;
                description = lib.mdDoc "Path to firmware config which is generated using `klipper-genconf`";
              };
            };
          });
      };
    };
  };

  ##### implementation
  config = mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.octoprintIntegration -> config.services.octoprint.enable;
        message = "Option services.klipper.octoprintIntegration requires Octoprint to be enabled on this system. Please enable services.octoprint to use it.";
      }
      {
        assertion = cfg.user != null -> cfg.group != null;
        message = "Option services.klipper.group is not set when services.klipper.user is specified.";
      }
      {
        assertion = cfg.settings != null -> foldl (a: b: a && b) true (mapAttrsToList (mcu: _: mcu != null -> (hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)) cfg.firmwares);
        message = "Option services.klipper.settings.$mcu.serial must be set when settings.klipper.firmware.$mcu is specified";
      }
      {
        assertion = (cfg.configFile != null) != (cfg.settings != null);
        message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
      }
    ];

    environment.etc = mkIf (!cfg.mutableConfig) {
      "klipper.cfg".source = if cfg.settings != null then format.generate "klipper.cfg" cfg.settings else cfg.configFile;
    };

    services.klipper = mkIf cfg.octoprintIntegration {
      user = config.services.octoprint.user;
      group = config.services.octoprint.group;
    };

    systemd.services.klipper =
      let
        klippyArgs = "--input-tty=${cfg.inputTTY}"
          + optionalString (cfg.apiSocket != null) " --api-server=${cfg.apiSocket}"
          + optionalString (cfg.logFile != null) " --logfile=${cfg.logFile}"
        ;
        printerConfigPath =
          if cfg.mutableConfig
          then cfg.mutableConfigFolder + "/printer.cfg"
          else "/etc/klipper.cfg";
        printerConfigFile =
          if cfg.settings != null
          then format.generate "klipper.cfg" cfg.settings
          else cfg.configFile;
      in
      {
        description = "Klipper 3D Printer Firmware";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];
        preStart = ''
          mkdir -p ${cfg.mutableConfigFolder}
          ${lib.optionalString (cfg.mutableConfig) ''
            [ -e ${printerConfigPath} ] || {
              cp ${printerConfigFile} ${printerConfigPath}
              chmod +w ${printerConfigPath}
            }
          ''}
          mkdir -p ${cfg.mutableConfigFolder}/gcodes
        '';

        serviceConfig = {
          ExecStart = "${cfg.package}/bin/klippy ${klippyArgs} ${printerConfigPath}";
          RuntimeDirectory = "klipper";
          StateDirectory = "klipper";
          SupplementaryGroups = [ "dialout" ];
          WorkingDirectory = "${cfg.package}/lib";
          OOMScoreAdjust = "-999";
          CPUSchedulingPolicy = "rr";
          CPUSchedulingPriority = 99;
          IOSchedulingClass = "realtime";
          IOSchedulingPriority = 0;
          UMask = "0002";
        } // (if cfg.user != null then {
          Group = cfg.group;
          User = cfg.user;
        } else {
          DynamicUser = true;
          User = "klipper";
        });
      };

    environment.systemPackages =
      with pkgs;
      let
        default = a: b: if a != null then a else b;
        firmwares = filterAttrs (n: v: v != null) (mapAttrs
          (mcu: { enable, enableKlipperFlash, configFile, serial }:
            if enable then
              pkgs.klipper-firmware.override
                {
                  mcu = lib.strings.sanitizeDerivationName mcu;
                  firmwareConfig = configFile;
                } else null)
          cfg.firmwares);
        firmwareFlasher = mapAttrsToList
          (mcu: firmware: pkgs.klipper-flash.override {
            mcu = lib.strings.sanitizeDerivationName mcu;
            klipper-firmware = firmware;
            flashDevice = default cfg.firmwares."${mcu}".serial cfg.settings."${mcu}".serial;
            firmwareConfig = cfg.firmwares."${mcu}".configFile;
          })
          (filterAttrs (mcu: firmware: cfg.firmwares."${mcu}".enableKlipperFlash) firmwares);
      in
      [ klipper-genconf ] ++ firmwareFlasher ++ attrValues firmwares;
  };
  meta.maintainers = [
    maintainers.cab404
  ];
}