summary refs log tree commit diff
path: root/nixos/modules/services/misc/klipper.nix
blob: fee7779141cdd9c80dfb976c37cf6f6d7de131d5 (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
{ 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 "Klipper, the 3D printer firmware";

      package = mkOption {
        type = types.package;
        default = pkgs.klipper;
        defaultText = literalExpression "pkgs.klipper";
        description = "The Klipper package.";
      };

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

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

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

      user = mkOption {
        type = types.nullOr types.str;
        default = null;
        description = ''
          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 = ''
          Group account under which Klipper runs.

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

      settings = mkOption {
        type = format.type;
        default = { };
        description = ''
          Configuration for Klipper. See the <link xlink:href="https://www.klipper3d.org/Overview.html#configuration-and-tuning-guides">documentation</link>
          for supported values.
        '';
      };

      firmwares = mkOption {
        description = "Firmwares klipper should manage";
        default = { };
        type = with types; attrsOf
          (submodule {
            options = {
              enable = mkEnableOption ''
                building of firmware and addition of klipper-flash tools for manual flashing.
                This will add `klipper-flash-$mcu` scripts to your environment which can be called to flash the firmware.
              '';
              configFile = mkOption {
                type = path;
                description = "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 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 klipper.group is not set when a user is specified.";
      }
      {
        assertion = foldl (a: b: a && b) true (mapAttrsToList (mcu: _: mcu != null -> (hasAttrByPath [ "${mcu}" "serial" ] cfg.settings)) cfg.firmwares);
        message = "Option klipper.settings.$mcu.serial must be set when klipper.firmware.$mcu is specified";
      }
    ];

    environment.etc."klipper.cfg".source = format.generate "klipper.cfg" cfg.settings;

    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}";
      in
      {
        description = "Klipper 3D Printer Firmware";
        wantedBy = [ "multi-user.target" ];
        after = [ "network.target" ];

        serviceConfig = {
          ExecStart = "${cfg.package}/lib/klipper/klippy.py ${klippyArgs} /etc/klipper.cfg";
          RuntimeDirectory = "klipper";
          SupplementaryGroups = [ "dialout" ];
          WorkingDirectory = "${cfg.package}/lib";
        } // (if cfg.user != null then {
          Group = cfg.group;
          User = cfg.user;
        } else {
          DynamicUser = true;
          User = "klipper";
        });
      };

    environment.systemPackages =
      with pkgs;
      let
        firmwares = filterAttrs (n: v: v!= null) (mapAttrs
          (mcu: { enable, configFile }: 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 = cfg.settings."${mcu}".serial;
            firmwareConfig = cfg.firmwares."${mcu}".configFile;
          })
          firmwares;
      in
      [ klipper-genconf ] ++ firmwareFlasher ++ attrValues firmwares;
  };
}