'=====================================================================
' M801 - Z 상면 영점 잡기  (버튼식 프로브 + 엔드밀 터치오프)
' 버튼식(블록형) Z 프로브 + Mach3 Mill 용
' 단위: mm (G21)
' ※ 한글 주석은 Mach3 VB 편집기(비유니코드)에서 깨져 보일 수 있으나
'    주석(')은 실행에서 무시되므로 동작에는 아무 영향이 없습니다.
'---------------------------------------------------------------------
' 어떤 방식인가
'   스핀들에는 가공에 쓸 엔드밀이 그대로 꽂혀 있고, 두께를 아는 버튼식
'   프로브를 소재 상면에 올려 놓고 그 버튼을 누르는 방식이다.
'   엔드밀 끝이 버튼에 닿은 지점은 소재 상면보다 정확히 PROBE_H 만큼 높다.
'   그래서 닿은 지점을 Z = PROBE_H 로 놓으면 소재 상면이 저절로 Z0 이 된다.
'   (스타일러스 볼 프로브가 아니므로 볼 지름·반경 보정은 쓰지 않는다)
'---------------------------------------------------------------------
' 사용법
'   1. 가공할 엔드밀을 스핀들에 꽂습니다.
'   2. 버튼식 프로브를 소재 상면에 올려 놓습니다.
'   3. 엔드밀 끝을 프로브 버튼 위 3~5mm 지점으로 조그 이동합니다.
'   4. MDI 창에:  M801
'   5. 엔드밀이 Z- 로 내려가 버튼을 누르면, 소재 상면이 Z 작업좌표 0 이
'      됩니다. 프로브를 치우면 그대로 가공을 시작할 수 있습니다.
'=====================================================================

'------------------------- 설정 (여기를 수정) --------------------------
PROBE_H      = 10.0     ' 프로브 높이(두께) [mm] - 닿은 지점에서 이만큼 빼면 소재 상면
FAST_F       = 150      ' 1차 탐색 이송속도 [mm/min] (Z 과이동 여유가 3mm 뿐이라 낮게 유지)
SLOW_F       = 15       ' 2차 정밀 측정 이송속도 [mm/min]
SEARCH_DIST  = 10.0     ' 최대 탐색 거리 [mm] - 이 안에 면이 없으면 중단
RETRACT      = 1.0      ' 2차 측정 전 후퇴량 [mm]
AVG_PASS     = 1        ' 저속 정밀 측정 반복 횟수 [회] - 2~3 이면 반복오차가 줄고, 그만큼 느려진다
CLEARANCE    = 3.0      ' 측정 완료 후 상승 높이 [mm]
USE_SCREEN_GAGE = 0     ' 1 = 위 PROBE_H 대신 Offsets(Alt5) 화면의 Gage Block Height 칸 값을 사용
                        '     (프로브를 여러 개 번갈아 쓸 때 매크로를 안 고치고 화면에서 바꾸는 용도)
USE_DRO      = 0        ' 0 = #2000~#2002 변수 사용(권장) | 1 = 정지 후 DRO 읽기
PROBE_LED    = 825      ' 프로브 입력에 해당하는 Mach3 OEM LED 번호
PROBE_CHECK  = 1        ' 1 = 움직이기 전에 프로브를 손으로 눌러 신호를 확인한다 (강력 권장)
                        '     장비·프로브를 연결하지 않은 상태에서 실행하면 여기서 걸러진다
MAX_TRAVEL   = 100      ' 한 번의 프로빙이 갈 수 있는 절대 상한 [mm] - 설정 실수로 폭주하는 것을 막는다
DRY_RUN      = 0        ' 1 = 장비 없이 흐름만 확인. G코드를 내보내지 않고 기록만 하며
                        '     축은 움직이지 않고 영점도 잡지 않는다. 숫자는 가상값이다
DRY_STEP     = 5.0      ' DRY_RUN 일 때 가상 트립까지의 거리 [mm] - 계산이 돌아가게만 하는 값
'---------------------------------------------------------------------

gErr = 0                         ' 전역 에러 플래그 (0=정상, 1=에러 발생)
gProbeOK = 0                     ' 프로브 응답 확인 여부 (실행당 1회)
gLog = ""                        ' DRY_RUN 기록
gLogN = 0

Emit "G21"                       ' 단위를 mm 로 (인치 방지)
Emit "G90"                       ' 절대좌표 모드로
WaitIdle                       ' 위 명령이 끝날 때까지 대기

' 시작 전 점검 : 프로브 입력(LED)이 이미 켜져 있지 않은지 확인한다.
' 정상이라면 엔드밀 끝이 버튼에서 떨어져 있어 LED 는 꺼짐(0)이어야 한다.
' 켜져 있다면 배선 오류 / Active-Low 설정 반대 / 버튼이 이미 눌린 상태라는
' 뜻이고, 그대로 G31 을 실행하면 접촉을 감지하지 못한 채 계속 밀고 들어가
' 프로브와 엔드밀이 함께 망가지므로 시작 전에 걸러낸다.
If DRY_RUN = 0 And GetOemLed(PROBE_LED) <> 0 Then
  MsgBox "Probe input is ALREADY active." & Chr(13) & "Check wiring / Active-Low setting, or lift the tool off the probe button."
  End                            ' 매크로 즉시 종료
End If

Message "M801 : probing Z surface ..."      ' 화면 하단 상태줄에 진행 표시
zt = ProbeAxis(2, -1, SEARCH_DIST)   ' Z축(2)을 -방향(아래)으로 프로빙, 접촉 좌표를 zt 에 저장

' 방금 프로빙이 실패했으면(gErr=1) 여기서 즉시 멈춘다.
' 이유 : 실패를 무시하고 계속하면 틀린 좌표로 영점이 잡히고,
' 그 영점으로 가공하면 공구·공작물 충돌 사고로 이어지기 때문.
If gErr <> 0 Then                ' 프로빙 중 에러가 났으면 (접촉 실패 등)
  Message "M801 aborted."        ' 중단 안내 표시 후
  End                            ' 매크로 즉시 종료
End If

' 화면 값 연동 : 프로브를 여러 개 번갈아 쓸 때 매크로를 매번 고치지 않도록,
' Offsets(Alt5) 화면의 Gage Block Height 칸(표준 1024 스크린셋 OEM DRO 1001)에
' 프로브 높이를 적어 두고 그 값을 쓸 수 있게 한다.
' 화면 값이 0 이면 적어 두는 것을 잊은 경우다. 그대로 쓰면 소재 상면보다
' 프로브 높이만큼 위가 Z0 이 되어 가공이 허공에서 이뤄지므로 시작 전에 막는다.
If USE_SCREEN_GAGE = 1 Then
  PROBE_H = GetOemDro(1001)
  If PROBE_H <= 0 Then
    MsgBox "Gage Block Height on the Offsets screen is 0." & Chr(13) & "Enter the probe height there, or set USE_SCREEN_GAGE = 0."
    Message "M801 aborted : probe height is 0."
    End                          ' 매크로 즉시 종료
  End If
End If

' 엔드밀 끝이 버튼에 닿은 지점은 소재 상면보다 PROBE_H 만큼 높다.
' 그러므로 닿은 지점을 Z = PROBE_H 로 놓으면 소재 상면이 Z0 이 된다.
' (엔드밀 끝면이 그대로 닿으므로 볼 반경 같은 보정은 없다)
zcur = GetOemDro(802)                       ' 현재 Z 작업좌표(DRO) 읽기
SetZero 802, zcur - zt + PROBE_H    ' "현재값 - 트립값" = 닿은 지점이 0, 여기에 +PROBE_H
WaitIdle                       ' DRO 반영 대기

Emit "G91 G0 Z" & CLEARANCE      ' 상대이동으로 CLEARANCE 만큼 위로 상승 (프로브 보호)
WaitIdle
Emit "G90"                       ' 절대좌표 모드 복귀

Message "M801 done : stock top = Z0 (probe height " & PROBE_H & " mm)"   ' 완료 안내
DryReport "M801"
End                              ' 매크로 정상 종료

'=====================================================================
'  보조 루틴
'=====================================================================

'---------------------------------------------------------------------
' DRY_RUN 문지기 : 기계로 나가는 출구를 한곳으로 모은다.
'
' 매크로가 바깥 세상에 영향을 주는 길은 둘뿐이다 - G코드를 보내는 것(Code)과
' 영점 DRO 를 쓰는 것(SetOemDro). 그 둘만 막으면 나머지 계산은 아무리 돌려도
' 안전하다. 그래서 모든 호출을 Emit / SetZero 로 바꿔 두었다.
'---------------------------------------------------------------------
Sub Emit(s)
  If DRY_RUN = 1 Then
    ' 로그가 끝없이 길어지면 완료창에 담기지 않으므로 앞부분만 남긴다
    If gLogN < 40 Then
      gLog = gLog & s & Chr(13)
      gLogN = gLogN + 1
    ElseIf gLogN = 40 Then
      gLog = gLog & "... (이하 생략)" & Chr(13)
      gLogN = gLogN + 1
    End If
  Else
    Code s
  End If
End Sub

' 영점 설정 - DRY_RUN 에서는 절대 쓰지 않는다.
' 가상 좌표로 X0 Y0 을 잡아 버리면 진짜 영점인 줄 알고 가공하게 되므로,
' 이 모드의 가장 큰 위험이 바로 이 지점이다.
Sub SetZero(n, v)
  If DRY_RUN = 1 Then
    gLog = gLog & "[DRY RUN] SetOemDro(" & n & ") 생략 - 영점을 잡지 않았습니다" & Chr(13)
  Else
    Call SetOemDro(n, v)
  End If
End Sub

' 트립 좌표 읽기 - DRY_RUN 에서는 현재 위치에서 DRY_STEP 만큼 떨어진 가상값.
' 실제로 움직이지 않으므로 모든 축이 시작점을 기준으로 계산된다.
Function ReadTrip(i, sgn)
  If DRY_RUN = 1 Then
    ReadTrip = GetOemDro(800 + i) + sgn * DRY_STEP
  Else
    ReadTrip = TripPos(i)
  End If
End Function

' DRY_RUN 완료 보고 - 진짜 측정과 눈으로 구분되어야 한다
Sub DryReport(mac)
  If DRY_RUN <> 1 Then Exit Sub
  MsgBox "[DRY RUN] " & mac & Chr(13) & Chr(13) & "Nothing moved. No zero was set. Numbers are synthetic." & Chr(13) & Chr(13) & gLog
  Message "[DRY RUN] " & mac & " - nothing moved"
End Sub

' 완료 보고 - DRY_RUN 에서는 아무것도 하지 않았으므로 이 창을 띄우면 안 된다.
' 종전에 "Bore centre set to X0 Y0" 이 그대로 떠서, 영점을 잡은 것처럼 보였다.
Sub Done(s)
  If DRY_RUN = 1 Then Exit Sub    ' DryReport 가 대신 사실대로 보고한다
  MsgBox s
End Sub

' 이동이 완전히 끝날 때까지 대기.
' G31 직후 좌표를 읽으려면 기계가 완전히 멈춘 뒤여야 하기 때문.
Sub WaitIdle()
  Sleep(200)                     ' 0.2초 기본 대기 (명령 전달 시간)
  While IsMoving()               ' 축이 아직 움직이는 동안
    Sleep(20)                    ' 0.02초씩 기다리며 반복 확인
  Wend
End Sub

' 축 번호(0/1/2) -> 축 이름("X"/"Y"/"Z") 변환.
' 이유 : G코드 명령은 "G31 X-12" 처럼 축 문자로 조립해야 하는데,
' 계산 로직은 숫자 번호가 편하므로 둘을 이어 주는 다리 역할.
Function AxisName(i)
  If i = 0 Then AxisName = "X"
  If i = 1 Then AxisName = "Y"
  If i = 2 Then AxisName = "Z"
End Function

' 트립(접촉 순간) 좌표 읽기
' 기본(USE_DRO=0)은 Mach3 가 G31 때 자동 기록하는 #2000~#2002 변수 사용
Function TripPos(i)
  If USE_DRO = 1 Then
    TripPos = GetOemDro(800 + i) ' 정지 후 DRO 에서 읽기 (구형 설정용)
  Else
    TripPos = GetVar(2000 + i)   ' G31 트립 변수에서 읽기 (권장)
  End If
End Function


'---------------------------------------------------------------------
' 안전장치 : 프로브가 실제로 신호를 내는지 "움직이기 전에" 확인한다.
'
' 종전에는 "이미 눌려 있는가" 만 보았다. 그래서 프로브를 연결하지 않아
' 입력이 영원히 0 인 경우를 그대로 통과시켰고, G31 은 트립을 받지 못한 채
' 탐색 거리를 끝까지 밀고 나갔다. 개발·시험 중에 장비를 연결하지 않는 것은
' 흔한 상황이므로, 그 상태를 매크로가 스스로 알아채고 멈춰야 한다.
'
' 확인은 매크로 실행당 한 번만 한다 (gProbeOK). 축마다 손을 대라고 하면
' 쓸 수 없는 도구가 된다.
' 반환 : 1 = 진행해도 좋음 / 0 = 중단 (gErr 이 함께 켜진다)
'---------------------------------------------------------------------
Function ProbeReady(ax)
  Dim i, seen
  ProbeReady = 0

  ' DRY_RUN 은 축을 움직이지 않으므로 확인할 것이 없다
  If DRY_RUN = 1 Then
    gProbeOK = 1
    ProbeReady = 1
    Exit Function
  End If

  ' (1) 이미 눌린 상태면 G31 이 즉시 트립되어 엉뚱한 좌표가 잡힌다
  If GetOemLed(PROBE_LED) <> 0 Then
    MsgBox "Probe input is ALREADY active on " & ax & "." & Chr(13) & "Check wiring / Active-Low setting, or move the tool off the probe." & Chr(13) & "Nothing has moved."
    gErr = 1
    Exit Function
  End If

  ' (2) 이번 실행에서 이미 확인했으면 넘어간다
  If gProbeOK = 1 Then
    ProbeReady = 1
    Exit Function
  End If

  ' (3) 확인을 끈 경우 (실기에서 반복 작업할 때만 0 으로 두십시오)
  If PROBE_CHECK <> 1 Then
    gProbeOK = 1
    ProbeReady = 1
    Exit Function
  End If

  ' (4) 손으로 눌러 신호가 실제로 들어오는지 본다. 15초 안에 반응이 없으면 중단.
  '     이 단계에서는 축을 전혀 움직이지 않는다.
  seen = 0
  i = 0
  Message "SAFETY : press the probe tip by hand now (15 s) - nothing will move"
  While i < 150 And seen = 0
    If GetOemLed(PROBE_LED) <> 0 Then
      seen = 1
    End If
    Sleep(100)
    i = i + 1
  Wend

  If seen = 0 Then
    MsgBox "Probe signal NOT detected." & Chr(13) & Chr(13) & "Nothing has moved - the macro stopped before any motion." & Chr(13) & "Check: probe wiring, Config > Ports and Pins > Input Signals > Probe (enabled / port / pin / Active Low)." & Chr(13) & "PROBE_LED is set to " & PROBE_LED & " - verify it matches your screen set." & Chr(13) & Chr(13) & "Set PROBE_CHECK = 0 only if you are certain the probe works."
    Message "aborted : probe signal not detected (nothing moved)"
    gErr = 1
    Exit Function
  End If

  ' (5) 손을 떼기를 기다린다. 눌린 채로 G31 을 걸면 0mm 에서 트립된다.
  i = 0
  Message "SAFETY : release the probe ..."
  While i < 150 And GetOemLed(PROBE_LED) <> 0
    Sleep(100)
    i = i + 1
  Wend
  If GetOemLed(PROBE_LED) <> 0 Then
    MsgBox "Probe is still triggered after 15 s." & Chr(13) & "Nothing has moved. Check the probe and try again."
    gErr = 1
    Exit Function
  End If

  gProbeOK = 1
  ProbeReady = 1
  Message "SAFETY : probe signal OK - probing starts"
End Function

'---------------------------------------------------------------------
' 한 축 2단계 프로빙 (빠른 탐색 -> 후퇴 -> 저속 정밀 측정)
'   i    : 축 번호 0=X 1=Y 2=Z
'   sgn  : 진행 방향 +1 또는 -1
'   dist : 최대 탐색 거리 [mm]
' 반환   : 현재 작업좌표계 기준 트립(접촉) 좌표
' 왜 두 번 재는가 : 빠른 속도로 재면 관성 때문에 부정확하므로,
' 일단 빠르게 찾고 -> 1mm 물러난 뒤 -> 아주 느리게 다시 재서
' 정밀도를 확보한다.
'---------------------------------------------------------------------
Function ProbeAxis(i, sgn, dist)
  Dim ax, t, k, acc, n           ' ax = 축 이름 문자열, t = 트립 좌표 임시 저장
  ax = AxisName(i)               ' 0/1/2 -> "X"/"Y"/"Z"
  ProbeAxis = 0                  ' 반환값을 일단 0 으로 초기화

  ' 안전장치 : 프로브가 실제로 신호를 내는지 확인한 뒤에만 움직인다
  If ProbeReady(ax) = 0 Then Exit Function

  ' 탐색 거리 상한 - 설정 실수로 축이 멀리 밀려나는 것을 막는다
  If dist > MAX_TRAVEL Then
    MsgBox "Search distance " & dist & " mm exceeds MAX_TRAVEL " & MAX_TRAVEL & " mm on " & ax & "." & Chr(13) & "Nothing has moved. Lower the setting or raise MAX_TRAVEL."
    gErr = 1
    Exit Function
  End If

  ' --- 1차 : 빠른 탐색 - G31(프로빙 이동)으로 접촉할 때까지 전진
  ' G91=상대좌표, F=이송속도. 예) 완성된 명령 = "G91 G31 Z-10 F150"
  Emit "G91 G31 " & ax & (sgn * dist) & " F" & FAST_F
  WaitIdle                     ' 이동 종료 대기
  Emit "G90"                     ' 절대좌표 모드 복귀

  ' 탐색 거리 안에서 접촉이 없었으면(LED 꺼진 채) 위치/배선 문제 -> 중단
  If DRY_RUN = 0 And GetOemLed(PROBE_LED) = 0 Then
    MsgBox "No contact within " & dist & " mm on " & ax & ". Aborted."
    gErr = 1
    Exit Function
  End If

  ' --- 저속 정밀 측정 : AVG_PASS 회 반복해 평균 ---
  ' 왜 여러 번 재는가 : 프로브는 같은 자리를 다시 재도 트리거 위치가 미세하게 흔들린다.
  ' N 번 재서 평균하면 그 흔들림(반복정밀도)이 대략 1/sqrt(N) 로 줄어든다.
  ' 다만 측정 시간이 N 배로 늘고, 선접촉(pretravel) 처럼 한쪽으로 치우친 오차는
  ' 평균해도 줄지 않는다. 그래서 2~3 회가 실용적인 상한이다.
  ' AVG_PASS = 1 이면 종전과 완전히 같은 동작이다.
  acc = 0
  n = 0
  For k = 1 To AVG_PASS
    ' 접촉면에서 RETRACT 만큼 후퇴 - 다음 저속 측정 준비
    Emit "G91 G0 " & ax & (-sgn * RETRACT)
    WaitIdle
    Emit "G90"

    ' 저속 정밀 프로빙 - 후퇴량의 2배 거리를 천천히 전진
    ' 최종 정확도는 이 저속 패스가 결정한다
    Emit "G91 G31 " & ax & (sgn * RETRACT * 2) & " F" & SLOW_F
    WaitIdle
    Emit "G90"

    ' 접촉을 놓쳤으면 중단 (진동/칩 오염 등 드문 경우)
    If DRY_RUN = 0 And GetOemLed(PROBE_LED) = 0 Then
      MsgBox "Slow pass " & k & " of " & AVG_PASS & " lost contact on " & ax & ". Aborted."
      gErr = 1
      Exit Function
    End If

    acc = acc + ReadTrip(i, sgn)       ' 접촉 순간의 좌표를 더해 둔다
    n = n + 1
  Next
  t = acc / n                    ' 평균이 이 축의 최종 측정값

  ' --- 버튼에서 1mm 떨어져 마무리 (프로브·엔드밀 보호)
  Emit "G91 G0 " & ax & (-sgn * RETRACT)
  WaitIdle
  Emit "G90"

  ProbeAxis = t                  ' 트립 좌표를 반환
End Function
