aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/extras/fan.py
blob: 081db6422a422d9a3f6332f0de8be33bc2f9327f (plain)
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
# Printer cooling fan
#
# Copyright (C) 2016-2024  Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import pulse_counter, output_pin


class Fan:
    def __init__(self, config, default_shutdown_speed=0.0):
        self.printer = config.get_printer()
        self.last_fan_value = self.last_req_value = 0.0
        # Read config
        self.max_power = config.getfloat("max_power", 1.0, above=0.0, maxval=1.0)
        self.kick_start_time = config.getfloat("kick_start_time", 0.1, minval=0.0)
        self.off_below = config.getfloat(
            "off_below", default=0.0, minval=0.0, maxval=1.0
        )
        cycle_time = config.getfloat("cycle_time", 0.010, above=0.0)
        hardware_pwm = config.getboolean("hardware_pwm", False)
        shutdown_speed = config.getfloat(
            "shutdown_speed", default_shutdown_speed, minval=0.0, maxval=1.0
        )
        # Setup pwm object
        ppins = self.printer.lookup_object("pins")
        self.mcu_fan = ppins.setup_pin("pwm", config.get("pin"))
        self.mcu_fan.setup_max_duration(0.0)
        self.mcu_fan.setup_cycle_time(cycle_time, hardware_pwm)
        shutdown_power = max(0.0, min(self.max_power, shutdown_speed))
        self.mcu_fan.setup_start_value(0.0, shutdown_power)

        self.enable_pin = None
        enable_pin = config.get("enable_pin", None)
        if enable_pin is not None:
            self.enable_pin = ppins.setup_pin("digital_out", enable_pin)
            self.enable_pin.setup_max_duration(0.0)

        # Create gcode request queue
        self.gcrq = output_pin.GCodeRequestQueue(
            config, self.mcu_fan.get_mcu(), self._apply_speed
        )

        # Setup tachometer
        self.tachometer = FanTachometer(config)

        # Register callbacks
        self.printer.register_event_handler(
            "gcode:request_restart", self._handle_request_restart
        )

    def get_mcu(self):
        return self.mcu_fan.get_mcu()

    def _apply_speed(self, print_time, value):
        if value < self.off_below:
            value = 0.0
        value = max(0.0, min(self.max_power, value * self.max_power))
        if value == self.last_fan_value:
            return "discard", 0.0
        if self.enable_pin:
            if value > 0 and self.last_fan_value == 0:
                self.enable_pin.set_digital(print_time, 1)
            elif value == 0 and self.last_fan_value > 0:
                self.enable_pin.set_digital(print_time, 0)
        if (
            value
            and self.kick_start_time
            and (not self.last_fan_value or value - self.last_fan_value > 0.5)
        ):
            # Run fan at full speed for specified kick_start_time
            self.last_req_value = value
            self.last_fan_value = self.max_power
            self.mcu_fan.set_pwm(print_time, self.max_power)
            return "delay", self.kick_start_time
        self.last_fan_value = self.last_req_value = value
        self.mcu_fan.set_pwm(print_time, value)

    def set_speed(self, value, print_time=None):
        self.gcrq.send_async_request(value, print_time)

    def set_speed_from_command(self, value):
        self.gcrq.queue_gcode_request(value)

    def _handle_request_restart(self, print_time):
        self.set_speed(0.0, print_time)

    def get_status(self, eventtime):
        tachometer_status = self.tachometer.get_status(eventtime)
        return {
            "speed": self.last_req_value,
            "rpm": tachometer_status["rpm"],
        }


class FanTachometer:
    def __init__(self, config):
        printer = config.get_printer()
        self._freq_counter = None

        pin = config.get("tachometer_pin", None)
        if pin is not None:
            self.ppr = config.getint("tachometer_ppr", 2, minval=1)
            poll_time = config.getfloat("tachometer_poll_interval", 0.0015, above=0.0)
            sample_time = 1.0
            self._freq_counter = pulse_counter.FrequencyCounter(
                printer, pin, sample_time, poll_time
            )

    def get_status(self, eventtime):
        if self._freq_counter is not None:
            rpm = self._freq_counter.get_frequency() * 30.0 / self.ppr
        else:
            rpm = None
        return {"rpm": rpm}


class PrinterFan:
    def __init__(self, config):
        self.fan = Fan(config)
        # Register commands
        gcode = config.get_printer().lookup_object("gcode")
        gcode.register_command("M106", self.cmd_M106)
        gcode.register_command("M107", self.cmd_M107)

    def get_status(self, eventtime):
        return self.fan.get_status(eventtime)

    def cmd_M106(self, gcmd):
        # Set fan speed
        value = gcmd.get_float("S", 255.0, minval=0.0) / 255.0
        self.fan.set_speed_from_command(value)

    def cmd_M107(self, gcmd):
        # Turn fan off
        self.fan.set_speed_from_command(0.0)


def load_config(config):
    return PrinterFan(config)