diff --git a/fabaccess_klipper.py b/fabaccess_klipper.py new file mode 100644 index 0000000..e3b347a --- /dev/null +++ b/fabaccess_klipper.py @@ -0,0 +1,56 @@ +import logging +import requests + + +class FabCon: + def __init__(self, config): + self.printer = printer = config.get_printer() + self.api_ip = config.get('ip') + self.api_key = config.get('key', '') + self.machine = config.get('machine') + self.in_use = False + + + self.printer.register_event_handler("action:cancel", + self._handle_free) + self.printer.register_event_handler("action:start", + self._handle_check) + self.printer.register_event_handler("action:complete", + self._handle_free) + self.printer.register_event_handler("klippy:ready", + self._handle_startup) + + def _handle_check(self): + logging.info("FABACCESS CHECK") + try: + req = requests.get("http://" + self.api_ip + "/in_use/" + self.machine, timeout=1) + if req.text == "in_use": + logging.info("FABACCESS Machine in Use, Proceeding") + self.in_use = True + elif req.text == "free": + logging.info("FABACCESS Machine not registered to User, Canceling") + self.printer.invoke_shutdown("FABACCESS Machine not registered to User") + except: + logging.warning("FABACCESS ERROR") + + def _handle_free(self): + logging.info("FABACCESS FREE") + try: + req = requests.get("http://" + self.api_ip + "/free/" + self.machine, timeout=1) + logging.info("FABACCESS Machine freed") + self.in_use = False + except: + logging.warning("FABACCESS ERROR") + + def _handle_startup(self): + logging.info("FABACCESS LOADED") + + def get_status(self, eventtime): + data = { + 'in_use': self.in_use + } + return data + + +def load_config(config): + return FabCon(config) \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a21baf5 --- /dev/null +++ b/install.sh @@ -0,0 +1,178 @@ +#!/bin/bash + + +# Copied from the install.sh script the klipper_z_calibration project by protoloft +# https://github.com/protoloft/klipper_z_calibration/tree/master + + +SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/ && pwd )" +KLIPPER_PATH="${HOME}/klipper" +SYSTEMDDIR="/etc/systemd/system" +MOONRAKER_CONFIG="${HOME}/printer_data/config/moonraker.conf" +MOONRAKER_FALLBACK="${HOME}/klipper_config/moonraker.conf" +NUM_INSTALLS=0 + +# Force script to exit if an error occurs +set -e + +# Step 1: Check for root user +verify_ready() +{ + # check for root user + if [ "$EUID" -eq 0 ]; then + echo "This script must not run as root" + exit -1 + fi + # output used number of installs + if [[ $NUM_INSTALLS == 0 ]]; then + echo "Defaulted to one klipper install, if more than one instance, use -n" + else + echo "Number of Installs Selected: $NUM_INSTALLS" + fi + # Fall back to old config + if [ ! -f "$MOONRAKER_CONFIG" ]; then + echo "${MOONRAKER_CONFIG} does not exist. Falling back to ${MOONRAKER_FALLBACK}" + MOONRAKER_CONFIG="$MOONRAKER_FALLBACK" + fi +} + +# Step 2: Verify Klipper has been installed +check_klipper() +{ + if [[ $NUM_INSTALLS == 0 ]]; then + if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F "klipper.service")" ]; then + echo "Klipper service found!" + else + echo "Klipper service not found, please install Klipper first" + exit -1 + fi + else + for (( klip = 1; klip<=$NUM_INSTALLS; klip++ )); do + if [ "$(sudo systemctl list-units --full -all -t service --no-legend | grep -F "klipper-$klip.service")" ]; then + echo "klipper-$klip.service found!" + else + echo "klipper-$klip.service NOT found, please ensure you've entered the correct number of klipper instances you're running!" + exit -1 + fi + done + fi +} + +# Step 3: Check folders +check_requirements() +{ + if [ ! -d "${KLIPPER_PATH}/klippy/extras/" ]; then + echo "Error: Klipper not found in directory: ${KLIPPER_PATH}. Exiting.." + exit -1 + fi + echo "Klipper found at ${KLIPPER_PATH}" + + if [ ! -f "$MOONRAKER_CONFIG" ]; then + echo "Error: Moonraker configuration not found: ${MOONRAKER_CONFIG}. Exiting.." + exit -1 + fi + echo "Moonraker configuration found at ${MOONRAKER_CONFIG}" + + apt install python3-requests +} + +# Step 4: Link extension to Klipper +link_extension() +{ + + echo -n "Linking extension to Klipper... " + ln -sf "${SRCDIR}/fabaccess_klipper.py" "${KLIPPER_PATH}/klippy/extras/fabaccess_klipper.py" + mv "${KLIPPER_PATH}/klippy/extras/print_stats.py" "${KLIPPER_PATH}/klippy/extras/print_stats.py.bak" + ln -sf "${SRCDIR}/print_stats.py" "${KLIPPER_PATH}/klippy/extras/print_stats.py" + echo "[OK]" +} + + +# Step 6: Add updater to moonraker.conf +add_updater() +{ + echo -n "Adding update manager to moonraker.conf... " + update_section=$(grep -c '\[update_manager[a-z ]* fabaccess_klipper\]' $MOONRAKER_CONFIG || true) + if [ "$update_section" -eq 0 ]; then + echo -e "\n[update_manager fabaccess_klipper]" >> "$MOONRAKER_CONFIG" + echo "type: git_repo" >> "$MOONRAKER_CONFIG" + echo "path: ${SRCDIR}" >> "$MOONRAKER_CONFIG" + echo "origin: https://github.com/Tengo10/fabaccess_klipper.git" >> "$MOONRAKER_CONFIG" + echo "managed_services: klipper" >> "$MOONRAKER_CONFIG" + echo -e "\n" >> "$MOONRAKER_CONFIG" + echo "[OK]" + + echo -n "Restarting Moonraker... " + sudo systemctl restart moonraker + echo "[OK]" + else + echo "[SKIPPED]" + fi +} + +# Step 7: Restarting Klipper +restart_klipper() +{ + if [[ $NUM_INSTALLS == 0 ]]; then + echo -n "Restarting Klipper... " + sudo systemctl restart klipper + echo "[OK]" + else + for (( klip = 1; klip<=$NUM_INSTALLS; klip++)); do + echo -n "Restarting Klipper-$klip... " + sudo systemctl restart klipper-$klip + echo "[OK]" + done + fi +} + +uinstall() +{ + if [ -f "${KLIPPER_PATH}/klippy/extras/fabaccess_klipper.py" ]; then + echo -n "Uninstalling fabaccess_klipper... " + rm -f "${KLIPPER_PATH}/klippy/extras/fabaccess_klipper.py" + rm -f "${KLIPPER_PATH}/klippy/extras/fabaccess_klipper.pyc" + echo "[OK]" + echo "You can now remove the \"[update_manager fabaccess_klipper]\" section in your moonraker.conf and delete this directory." + echo "You also need to remove the \"[fabaccess_klipper]\" section in your Klipper configuration..." + else + echo -n "${KLIPPER_PATH}/klippy/extras/fabaccess_klipper.py not found. Is it installed? " + echo "[FAILED]" + fi +} + +usage() +{ + echo "Usage: $(basename $0) [-k ] [-m ] [-n ] [-u]" 1>&2; + exit 1; +} + +# Command parsing +while getopts ":k:m:n:uh" OPTION; do + case "$OPTION" in + k) KLIPPER_PATH="$OPTARG" ;; + m) MOONRAKER_CONFIG="$OPTARG" ;; + n) NUM_INSTALLS="$OPTARG" ;; + u) UNINSTALL=1 ;; + h | ?) usage ;; + esac +done + +# Fall back to old config +if [ ! -f "$MOONRAKER_CONFIG" ]; then + echo "${MOONRAKER_CONFIG} does not exist. Falling back to ${MOONRAKER_FALLBACK}" + MOONRAKER_CONFIG="$MOONRAKER_FALLBACK" +fi + +# Run steps +verify_ready +check_klipper +check_requirements +remove_service +if [ ! $UNINSTALL ]; then + link_extension + add_updater +else + uinstall +fi +restart_klipper \ No newline at end of file diff --git a/print_stats.py b/print_stats.py new file mode 100644 index 0000000..db5f6cb --- /dev/null +++ b/print_stats.py @@ -0,0 +1,128 @@ +# Virtual SDCard print stat tracking +# +# Copyright (C) 2020 Eric Callahan +# +# This file may be distributed under the terms of the GNU GPLv3 license. + + +# Modified to send events action:start, action:complete, action:cancel + + +class PrintStats: + def __init__(self, config): + printer = config.get_printer() + self.printer = 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) + 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.printer.send_event("action:start") + 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.printer.send_event("action:complete") + self._note_finish("complete") + def note_error(self, message): + self._note_finish("error", message) + def note_cancel(self): + self.printer.send_event("action:cancel") + 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. + self.filament_used = self.total_duration = 0. + self.print_start_time = self.last_pause_time = None + self.init_duration = 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)