# coding: utf-8
# python 2 only

# Copyright (c) 2026 TormachTips.com. All rights reserved.
# Licensed under the TormachTips Personal Use License.
# Permission is granted only for private personal use and private personal modification.
# No sharing, publication, distribution, resale, sublicensing, screenshots, code excerpts,
# benchmarks, or videos are permitted without prior written permission.
# Requests:         tormach.1100m@gmail.com
# Information page: https://tormachtips.com/plugins.htm

#-----------------------------------------------------------------------#
# Copyright © 2022 XoomSpeed All rights reserved.                       #
# License: GPL Version 2                                                #   
# Written by David Loomes 7/10/22                                       #
#-----------------------------------------------------------------------#

############################################
##                                        ##
##        Modified ProbeGuard 1.07        ##
##          www.tormachtips.com           ##
##                                        ##
############################################

# 1.07 - fixed utf encoding bug - 4/19/2026
# 1.06 - for cradles with limit switches and UI boards, adds automatic tool changing in PathPilot - 4/17/26

from threading import Thread
import glib
import singletons
import time
import linuxcnc
import hal
import constants
import os
import subprocess
from ui_hooks import plugin

CURRENT_VER         = "1.07"
SCRIPT_NAME         = "ProbeGuard in Cradle + M6 Changer Plugin"
DESCRIPTION         = "Modified ProbeGuard with cradle switch protection + auto T0/T99 (low CPU)"
PROBE_TOOL_NUMBERS  = (99,)
POLL_INTERVAL       = 0.25     
ENABLED             = 1
DEV_MACHINE         = 1
DEV_MACHINE_FLAG    = "/home/operator/gcode/python/dev_machine.txt"
BEEP_WAV_PATH       = "/home/operator/gcode/images/beep.wav"
AUDIO_CONFIRMATION  = 1

class UserPlugin(plugin):
    def __init__(self):
        plugin.__init__(self, 'Probe guard %s' % CURRENT_VER)
        dev_machine_found = os.path.exists(DEV_MACHINE_FLAG)
        if dev_machine_found:
            plugin_enabled = DEV_MACHINE
        else:
            plugin_enabled = ENABLED
        if plugin_enabled:
            self.error_handler.write("Modified ProbeGuard %s loaded - auto T0/T99 enabled" % CURRENT_VER, constants.ALARM_LEVEL_QUIET)
        else:
            if dev_machine_found:
                self.error_handler.write("Dev machine found. Plugin loaded, but disabled by DEV_MACHINE.", constants.ALARM_LEVEL_QUIET)
            else:
                self.error_handler.write("Plugin loaded, but disabled.", constants.ALARM_LEVEL_QUIET)
                self.error_handler.write("To enable, open script, find ENABLED = 0 and change to ENABLED = 1", constants.ALARM_LEVEL_QUIET)
            return
        self.ProbeGuard_hal = hal.component("ProbeGuard")
        self.ProbeGuard_hal.newpin("new-probe-enable", hal.HAL_BIT, hal.HAL_OUT)
        self.ProbeGuard_hal.newpin("old-probe-enable", hal.HAL_BIT, hal.HAL_IN)
        self.ProbeGuard_hal.ready()
        self.halStatus  = linuxcnc.stat()
        self.halCommand = linuxcnc.command()
        self.last_switch_closed = None
        self._tool_change_pending = False
        self._last_requested_tool = None        
        self.probeMonitorthread = Thread(target=self.monitorProbe)
        self.probeMonitorthread.daemon = True
        self.probeMonitorthread.start()
    
    def play_beep_async(self):
        try:
            if not AUDIO_CONFIRMATION:
                return
            if not os.path.isfile(BEEP_WAV_PATH):
                return
            subprocess.Popen(
                ["aplay", "-q", BEEP_WAV_PATH],
                stdout=open(os.devnull, "w"),
                stderr=open(os.devnull, "w"),
                close_fds=True            )
        except Exception as e:
            self.error_handler.write("[ProbeGuard] Audio confirmation failed: %s" % str(e),constants.ALARM_LEVEL_LOW)

    def handle_switch_edge(self, switch_closed):
        try:
            if self._tool_change_pending:
                return False
            ui = singletons.g_Machine
            if not ui:
                return False
            ui.status.poll()
            if ui.moving():
                return False
            if ui.status.task_mode == linuxcnc.MODE_AUTO:
                return False
            if switch_closed:
                target_tool = 0
                message = "Probe returned to cradle -> setting T0"
            else:
                target_tool = 99
                message = "Probe removed from cradle -> setting T99"
            self.play_beep_async()            
            if self._last_requested_tool == target_tool and ui.status.tool_in_spindle == target_tool:
                return False
            self._tool_change_pending = True
            self.error_handler.write("[ProbeGuard] " + message, constants.ALARM_LEVEL_QUIET)
            self.set_active_tool(target_tool)
        except Exception as e:
            self.error_handler.write("[ProbeGuard] Edge handler error: %s" % str(e),constants.ALARM_LEVEL_LOW)
        finally:
            self._tool_change_pending = False
        return False
    
    def monitorProbe(self):
        while True:
            try:
                self.halStatus.poll()
                toolIsAProbe = (self.halStatus.tool_in_spindle in PROBE_TOOL_NUMBERS) or (not self.halStatus.din[3])
                self.ProbeGuard_hal["new-probe-enable"] = (self.ProbeGuard_hal["old-probe-enable"] and (toolIsAProbe or self.halStatus.task_mode != linuxcnc.MODE_AUTO))
                if toolIsAProbe and self.halStatus.spindle_enabled:
                    self.halCommand.abort()
                    self.error_handler.write('Spindle halted - T99 selected or probe out of holder.',constants.ALARM_LEVEL_HIGH)
                switch_closed = bool(self.halStatus.din[3])
                if self.last_switch_closed is not None and switch_closed != self.last_switch_closed:
                    glib.idle_add(self.handle_switch_edge, switch_closed)
                self.last_switch_closed = switch_closed
            except Exception as e:
                self.error_handler.write("[ProbeGuard] Monitor error: %s" % str(e),constants.ALARM_LEVEL_LOW)
            time.sleep(POLL_INTERVAL)
    
    def set_active_tool(self, tool_num):
        try:
            ui = singletons.g_Machine
            if not ui:
                return
            if not ui.ensure_mode(linuxcnc.MODE_MDI):
                self.error_handler.write("[ProbeGuard] Could not enter MDI mode for tool update.",constants.ALARM_LEVEL_LOW)
                return
            ui.issue_mdi("M61 Q%d" % tool_num)
            ui.command.wait_complete()
            ui.issue_mdi("G43")
            ui.command.wait_complete()
            self._last_requested_tool = tool_num
        except Exception as e:
            self.error_handler.write("[ProbeGuard] Failed to set active tool: %s" % str(e),constants.ALARM_LEVEL_LOW)
