aboutsummaryrefslogtreecommitdiffstats
path: root/klippy/extras/print_stats.py
blob: e7b6e97ef181a02b82e3258eb07d1ca29228a9b0 (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
140
141
142
143
144
# Virtual SDCard print stat tracking
#
# Copyright (C) 2020  Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.


class PrintStats:
    def __init__(self, config):
        printer = config.get_printer()
        self.gcode_move = printer.load_object(config, "gcode_move")
        self.reactor = printer.get_reactor()
        self.reset()
        # Register commands
        self.gcode = printer.lookup_object("gcode")
        self.gcode.register_command(
            "SET_PRINT_STATS_INFO",
            self.cmd_SET_PRINT_STATS_INFO,
            desc=self.cmd_SET_PRINT_STATS_INFO_help,
        )
        printer.register_event_handler(
            "extruder:activate_extruder", self._handle_activate_extruder
        )

    def _handle_activate_extruder(self):
        gc_status = self.gcode_move.get_status()
        self.last_epos = gc_status["position"].e

    def _update_filament_usage(self, eventtime):
        gc_status = self.gcode_move.get_status(eventtime)
        cur_epos = gc_status["position"].e
        self.filament_used += (cur_epos - self.last_epos) / gc_status["extrude_factor"]
        self.last_epos = cur_epos

    def set_current_file(self, filename):
        self.reset()
        self.filename = filename

    def note_start(self):
        curtime = self.reactor.monotonic()
        if self.print_start_time is None:
            self.print_start_time = curtime
        elif self.last_pause_time is not None:
            # Update pause time duration
            pause_duration = curtime - self.last_pause_time
            self.prev_pause_duration += pause_duration
            self.last_pause_time = None
        # Reset last e-position
        gc_status = self.gcode_move.get_status(curtime)
        self.last_epos = gc_status["position"].e
        self.state = "printing"
        self.error_message = ""

    def note_pause(self):
        if self.last_pause_time is None:
            curtime = self.reactor.monotonic()
            self.last_pause_time = curtime
            # update filament usage
            self._update_filament_usage(curtime)
        if self.state != "error":
            self.state = "paused"

    def note_complete(self):
        self._note_finish("complete")

    def note_error(self, message):
        self._note_finish("error", message)

    def note_cancel(self):
        self._note_finish("cancelled")

    def _note_finish(self, state, error_message=""):
        if self.print_start_time is None:
            return
        self.state = state
        self.error_message = error_message
        eventtime = self.reactor.monotonic()
        self.total_duration = eventtime - self.print_start_time
        if self.filament_used < 0.0000001:
            # No positive extusion detected during print
            self.init_duration = self.total_duration - self.prev_pause_duration
        self.print_start_time = None

    cmd_SET_PRINT_STATS_INFO_help = (
        "Pass slicer info like layer act and " "total to klipper"
    )

    def cmd_SET_PRINT_STATS_INFO(self, gcmd):
        total_layer = gcmd.get_int("TOTAL_LAYER", self.info_total_layer, minval=0)
        current_layer = gcmd.get_int("CURRENT_LAYER", self.info_current_layer, minval=0)
        if total_layer == 0:
            self.info_total_layer = None
            self.info_current_layer = None
        elif total_layer != self.info_total_layer:
            self.info_total_layer = total_layer
            self.info_current_layer = 0

        if (
            self.info_total_layer is not None
            and current_layer is not None
            and current_layer != self.info_current_layer
        ):
            self.info_current_layer = min(current_layer, self.info_total_layer)

    def reset(self):
        self.filename = self.error_message = ""
        self.state = "standby"
        self.prev_pause_duration = self.last_epos = 0.0
        self.filament_used = self.total_duration = 0.0
        self.print_start_time = self.last_pause_time = None
        self.init_duration = 0.0
        self.info_total_layer = None
        self.info_current_layer = None

    def get_status(self, eventtime):
        time_paused = self.prev_pause_duration
        if self.print_start_time is not None:
            if self.last_pause_time is not None:
                # Calculate the total time spent paused during the print
                time_paused += eventtime - self.last_pause_time
            else:
                # Accumulate filament if not paused
                self._update_filament_usage(eventtime)
            self.total_duration = eventtime - self.print_start_time
            if self.filament_used < 0.0000001:
                # Track duration prior to extrusion
                self.init_duration = self.total_duration - time_paused
        print_duration = self.total_duration - self.init_duration - time_paused
        return {
            "filename": self.filename,
            "total_duration": self.total_duration,
            "print_duration": print_duration,
            "filament_used": self.filament_used,
            "state": self.state,
            "message": self.error_message,
            "info": {
                "total_layer": self.info_total_layer,
                "current_layer": self.info_current_layer,
            },
        }


def load_config(config):
    return PrintStats(config)