- 레시피 컨트롤박스에 'Scanner Cam' 버튼 추가 → 카메라+모션 창 Show/Hide 토글 - WDScannerCamMotion: 전용 displayType(Machine_C) 카메라뷰 + 매뉴얼 UCScannerMotion 재사용 - UCScannerMotionViewModel: publish/execute 모드 분리 - 레시피(publish): 입력값을 EVENT_SCANNER_COMMAND_MOVE 로 발행 - 매뉴얼(execute): 이벤트 구독해 동일 메소드로 실제 스캐너 이동(인터락 그대로 재사용) - Move/Jog/Home 메소드를 인자형으로 리팩터해 UI·이벤트 경로가 같은 본체 공유 - 분석 문서 추가(docs/분석/2026/06/23) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
450 lines
16 KiB
C#
450 lines
16 KiB
C#
using Aurora;
|
|
using Aurora.Api;
|
|
using Aurora.Api.Manager;
|
|
using Aurora.View;
|
|
using AuroraLaser;
|
|
using AuroraLaserScanner;
|
|
using HarfBuzzSharp;
|
|
using Microsoft.Office.Interop.Excel;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Reactive.Linq;
|
|
using System.Windows.Input;
|
|
using System.Windows.Threading;
|
|
|
|
namespace WeldingCommon.Views
|
|
{
|
|
public enum eAxisType
|
|
{
|
|
AXIS_X,
|
|
AXIS_Y
|
|
}
|
|
|
|
public enum eJogDirection
|
|
{
|
|
MINUS,
|
|
PLUS
|
|
}
|
|
|
|
public enum eMotionMode
|
|
{
|
|
ABS,
|
|
REL
|
|
};
|
|
|
|
public enum eScannerMoveKind
|
|
{
|
|
CommandPos,
|
|
Jog,
|
|
Home
|
|
}
|
|
|
|
/// <summary>
|
|
/// 레시피 화면(publish 모드)에서 매뉴얼 스캐너 VM(execute 모드)으로 보내는 모션 요청 페이로드.
|
|
/// 매뉴얼 VM의 저장 state가 아니라 이 값으로 동작하도록 모든 입력값을 담는다.
|
|
/// </summary>
|
|
public class ScannerCommandMoveParam
|
|
{
|
|
public eScannerMoveKind Kind;
|
|
public eAxisType Axis;
|
|
public eJogDirection Direction;
|
|
public double CommandPosX;
|
|
public double CommandPosY;
|
|
public double StepValue;
|
|
public eMotionMode Mode;
|
|
public double VelocityRate;
|
|
}
|
|
|
|
public class UCScannerMotionViewModel : NotifyPropertyChangedBase
|
|
{
|
|
private readonly Logger Log = new Logger("UCScannerMotionViewModel");
|
|
|
|
public ICommand CMD_VisibleChanged { get; }
|
|
public ICommand CMD_SpeedChanged { get; }
|
|
public ICommand CMD_MotionModeChanged { get; }
|
|
|
|
public ICommand CMD_JogMove { get; }
|
|
public ICommand CMD_CommandPosMove { get; }
|
|
|
|
|
|
private readonly IAuroraEnvironment _Aurora;
|
|
|
|
private readonly Scanner _Scanner;
|
|
private readonly Laser _Laser;
|
|
private readonly ScannerConfig _ScannerConfig;
|
|
|
|
private double _ScannerXPos;
|
|
public double ScannerXPos { get => _ScannerXPos; set => SetProperty(ref _ScannerXPos, value); }
|
|
private double _ScannerYPos;
|
|
public double ScannerYPos { get => _ScannerYPos; set => SetProperty(ref _ScannerYPos, value); }
|
|
private bool _IsScannerBusy;
|
|
public bool IsScannerBusy { get => _IsScannerBusy; set => SetProperty(ref _IsScannerBusy, value); }
|
|
|
|
private double _StepValue;
|
|
public double StepValue { get => _StepValue; set => SetProperty(ref _StepValue, value); }
|
|
|
|
private double _CommandPosX;
|
|
public double CommandPosX { get => _CommandPosX; set => SetProperty(ref _CommandPosX, value); }
|
|
private double _CommandPosY;
|
|
public double CommandPosY { get => _CommandPosY; set => SetProperty(ref _CommandPosY, value); }
|
|
|
|
private double _CommandPosLimitMinX;
|
|
public double CommandPosLimitMinX { get => _CommandPosLimitMinX; set => SetProperty(ref _CommandPosLimitMinX, value); }
|
|
private double _CommandPosLimitMaxX;
|
|
public double CommandPosLimitMaxX { get => _CommandPosLimitMaxX; set => SetProperty(ref _CommandPosLimitMaxX, value); }
|
|
|
|
private double _CommandPosLimitMinY;
|
|
public double CommandPosLimitMinY { get => _CommandPosLimitMinY; set => SetProperty(ref _CommandPosLimitMinY, value); }
|
|
private double _CommandPosLimitMaxY;
|
|
public double CommandPosLimitMaxY { get => _CommandPosLimitMaxY; set => SetProperty(ref _CommandPosLimitMaxY, value); }
|
|
|
|
private double _VelocityRate;
|
|
private eMotionMode _MotionMode;
|
|
|
|
// publish 모드: UI(레시피)는 직접 디바이스를 제어하지 않고, 입력값을 이벤트로 발행한다.
|
|
// execute 모드(기본, 매뉴얼): 이벤트를 구독해 실제 스캐너를 움직인다.
|
|
private readonly bool _PublishMode;
|
|
|
|
private readonly DispatcherTimer _MonitorTimer;
|
|
|
|
// PAM, RUW 처럼 2개 이상의 scanner 를 사용하는 경우가 있어, scannerId 를 받는 형태로 생성
|
|
public UCScannerMotionViewModel(IAuroraEnvironment aurora, string scannerId, bool publishMode = false)
|
|
{
|
|
_Aurora = aurora;
|
|
_PublishMode = publishMode;
|
|
|
|
_Scanner = _Aurora.DeviceManager[scannerId] as Scanner;
|
|
_Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as Laser;
|
|
_ScannerConfig = _Aurora.DeviceManager.GetDeviceConfig(scannerId) as ScannerConfig;
|
|
|
|
InitVariable();
|
|
|
|
CMD_VisibleChanged = new DelegateCommand(VisibleChanged);
|
|
CMD_SpeedChanged = new DelegateCommand(PerformCmdSpeedChanged);
|
|
CMD_MotionModeChanged = new DelegateCommand(PerformCmdMotionModeChanged);
|
|
CMD_JogMove = new DelegateCommand(PerformCmdJogMove);
|
|
CMD_CommandPosMove = new DelegateCommand(PerformCmdCommandPosMove);
|
|
|
|
// execute 모드만 원격 모션 요청을 구독한다(중복 실행 방지). UI 스레드에서 처리.
|
|
if (!_PublishMode) {
|
|
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_SCANNER_COMMAND_MOVE)
|
|
.ObserveOnDispatcher()
|
|
.Subscribe(OnRemoteMotionRequested);
|
|
}
|
|
|
|
_MonitorTimer = new DispatcherTimer();
|
|
_MonitorTimer.Tick += OnMonitoring;
|
|
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
|
|
}
|
|
|
|
private void PublishMotion(ScannerCommandMoveParam param)
|
|
{
|
|
_Aurora.EventPublisher.Publish(
|
|
new EventMessage("UCScannerMotionViewModel.Publish", CommonEventIds.EVENT_SCANNER_COMMAND_MOVE, param));
|
|
}
|
|
|
|
private void OnRemoteMotionRequested(EventMessage msg)
|
|
{
|
|
if (msg == null || !(msg.Param is ScannerCommandMoveParam p)) {
|
|
return;
|
|
}
|
|
|
|
switch (p.Kind) {
|
|
case eScannerMoveKind.CommandPos:
|
|
MoveCommandXYPos(p.Axis, p.CommandPosX, p.CommandPosY, p.Mode, p.VelocityRate);
|
|
break;
|
|
|
|
case eScannerMoveKind.Jog:
|
|
if (IsMainSequenceRunning()) return;
|
|
MoveJogXYPos(p.Axis, p.Direction, p.StepValue, p.VelocityRate);
|
|
break;
|
|
|
|
case eScannerMoveKind.Home:
|
|
if (IsMainSequenceRunning()) return;
|
|
MoveJogHome(p.VelocityRate);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private bool IsMainSequenceRunning()
|
|
{
|
|
return _Aurora.SequenceEntry[CommonSequenceIds.SeqAutoRun].RunState == SequenceRunState.Running;
|
|
}
|
|
|
|
private void InitVariable()
|
|
{
|
|
_VelocityRate = 1;
|
|
_MotionMode = eMotionMode.ABS;
|
|
}
|
|
|
|
private void VisibleChanged(object obj)
|
|
{
|
|
bool visible = (bool)obj;
|
|
if (visible) {
|
|
Init_XYLimitPos();
|
|
_MonitorTimer.Start();
|
|
} else {
|
|
_MonitorTimer.Stop();
|
|
}
|
|
}
|
|
|
|
private void Init_XYLimitPos()
|
|
{
|
|
// TODO: Limit 값을 Min, Max 로 따로 Setting 입력값으로 가져와야 할 수도 있음. 우선 현상태 유지
|
|
CommandPosLimitMinX = -1 * _ScannerConfig.ScanXSize / 2;
|
|
CommandPosLimitMaxX = _ScannerConfig.ScanXSize / 2;
|
|
|
|
CommandPosLimitMinY = -1 * _ScannerConfig.ScanYSize / 2;
|
|
CommandPosLimitMaxY = _ScannerConfig.ScanYSize / 2;
|
|
|
|
//CommandPosXLimit = string.Format($"{-1 * _ScannerConfig.ScanXSize / 2} ~ {_ScannerConfig.ScanXSize / 2}");
|
|
//CommandPosYLimit = string.Format($"{-1 * _ScannerConfig.ScanYSize / 2} ~ {_ScannerConfig.ScanYSize / 2}");
|
|
}
|
|
|
|
private void OnMonitoring(object sender, EventArgs e)
|
|
{
|
|
double xPos, yPos;
|
|
if (_Scanner != null && _Scanner.IsActivated) {
|
|
_Scanner.GetXYPos(out xPos, out yPos);
|
|
ScannerXPos = xPos;
|
|
ScannerYPos = yPos;
|
|
|
|
IsScannerBusy = _Scanner.GetBusyStatus();
|
|
} else {
|
|
ScannerXPos = 0;
|
|
ScannerYPos = 0;
|
|
IsScannerBusy = false;
|
|
}
|
|
|
|
}
|
|
|
|
private void PerformCmdSpeedChanged(object commandParameter)
|
|
{
|
|
switch ((string)commandParameter) {
|
|
case "Low": {
|
|
_VelocityRate = 0.5;
|
|
}
|
|
break;
|
|
|
|
case "Mid": {
|
|
_VelocityRate = 1;
|
|
}
|
|
break;
|
|
|
|
case "High": {
|
|
_VelocityRate = 1.5;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void PerformCmdMotionModeChanged(object commandParameter)
|
|
{
|
|
switch ((string)commandParameter) {
|
|
case "ABS": {
|
|
_MotionMode = eMotionMode.ABS;
|
|
}
|
|
break;
|
|
|
|
case "REL": {
|
|
_MotionMode = eMotionMode.REL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void PerformCmdJogMove(object commandParameter)
|
|
{
|
|
// TODO: Left, Right, Up, Down 은 장비 Scanner 의 방향에 따라 달라질 수 있다
|
|
// 양산중에는 동작 하면 안됨
|
|
if (IsMainSequenceRunning())
|
|
return;
|
|
|
|
eAxisType axis;
|
|
eJogDirection direction;
|
|
switch ((string)commandParameter) {
|
|
case "Left": axis = eAxisType.AXIS_X; direction = eJogDirection.MINUS; break;
|
|
case "Right": axis = eAxisType.AXIS_X; direction = eJogDirection.PLUS; break;
|
|
case "Up": axis = eAxisType.AXIS_Y; direction = eJogDirection.PLUS; break;
|
|
case "Down": axis = eAxisType.AXIS_Y; direction = eJogDirection.MINUS; break;
|
|
case "Home":
|
|
if (_PublishMode) {
|
|
PublishMotion(new ScannerCommandMoveParam {
|
|
Kind = eScannerMoveKind.Home,
|
|
VelocityRate = _VelocityRate,
|
|
});
|
|
} else {
|
|
MoveJogHome(_VelocityRate);
|
|
}
|
|
return;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (_PublishMode) {
|
|
PublishMotion(new ScannerCommandMoveParam {
|
|
Kind = eScannerMoveKind.Jog,
|
|
Axis = axis,
|
|
Direction = direction,
|
|
StepValue = StepValue,
|
|
VelocityRate = _VelocityRate,
|
|
});
|
|
return;
|
|
}
|
|
|
|
MoveJogXYPos(axis, direction, StepValue, _VelocityRate);
|
|
}
|
|
|
|
private void MoveJogXYPos(eAxisType axisType, eJogDirection direction, double stepValue, double velocityRate)
|
|
{
|
|
double xPos = 0.0;
|
|
double yPos = 0.0;
|
|
switch (axisType) {
|
|
case eAxisType.AXIS_X: {
|
|
if (direction == eJogDirection.MINUS) {
|
|
xPos = stepValue * -1;
|
|
yPos = 0;
|
|
} else if (direction == eJogDirection.PLUS) {
|
|
xPos = stepValue;
|
|
yPos = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case eAxisType.AXIS_Y: {
|
|
if (direction == eJogDirection.MINUS) {
|
|
xPos = 0;
|
|
yPos = stepValue * -1;
|
|
} else if (direction == eJogDirection.PLUS) {
|
|
xPos = 0;
|
|
yPos = stepValue;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 현재 Curruent Pos 기준으로, 입력방향으로 입력값이 Limit 를 넘어 서는지 확인 (Rel 임)
|
|
if (CheckOverLimitPos(ScannerXPos + xPos, ScannerYPos + yPos)) {
|
|
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
|
return;
|
|
}
|
|
|
|
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
|
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
|
_Scanner.JumpRel(xPos, yPos);
|
|
|
|
//Jump 후 Speed 원복
|
|
_Scanner.SetJumpSpeedCtrl(_ScannerConfig.JumpSpeed);
|
|
}
|
|
|
|
private void MoveJogHome(double velocityRate)
|
|
{
|
|
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
|
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
|
_Scanner.JumpAbs(0, 0);
|
|
|
|
//Jump 후 Speed 원복
|
|
_Scanner.SetJumpSpeedCtrl(_ScannerConfig.JumpSpeed);
|
|
}
|
|
|
|
private void PerformCmdCommandPosMove(object commandParameter)
|
|
{
|
|
eAxisType axis;
|
|
switch ((string)commandParameter) {
|
|
case "X": axis = eAxisType.AXIS_X; break;
|
|
case "Y": axis = eAxisType.AXIS_Y; break;
|
|
default: return;
|
|
}
|
|
|
|
if (_PublishMode) {
|
|
PublishMotion(new ScannerCommandMoveParam {
|
|
Kind = eScannerMoveKind.CommandPos,
|
|
Axis = axis,
|
|
CommandPosX = CommandPosX,
|
|
CommandPosY = CommandPosY,
|
|
Mode = _MotionMode,
|
|
VelocityRate = _VelocityRate,
|
|
});
|
|
return;
|
|
}
|
|
|
|
MoveCommandXYPos(axis, CommandPosX, CommandPosY, _MotionMode, _VelocityRate);
|
|
}
|
|
|
|
private void MoveCommandXYPos(eAxisType axisType, double commandPosX, double commandPosY, eMotionMode motionMode, double velocityRate)
|
|
{
|
|
double xPos = 0.0;
|
|
double yPos = 0.0;
|
|
switch (axisType) {
|
|
case eAxisType.AXIS_X: {
|
|
xPos = commandPosX;
|
|
|
|
if (motionMode == eMotionMode.ABS) {
|
|
yPos = _Scanner.GetCommandPosY();
|
|
} else {
|
|
yPos = 0.0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case eAxisType.AXIS_Y: {
|
|
if (motionMode == eMotionMode.ABS) {
|
|
xPos = _Scanner.GetCommandPosX();
|
|
} else {
|
|
xPos = 0.0;
|
|
}
|
|
|
|
yPos = commandPosY;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 현재 Curruent Pos 기준으로, 입력방향으로 입력값이 Limit 를 넘어 서는지 확인
|
|
if (motionMode == eMotionMode.ABS) {
|
|
if (CheckOverLimitPos(xPos, yPos)) {
|
|
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
|
return;
|
|
}
|
|
} else {
|
|
if (CheckOverLimitPos(ScannerXPos + xPos, ScannerYPos + yPos)) {
|
|
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
|
return;
|
|
}
|
|
}
|
|
|
|
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
|
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
|
|
|
if (motionMode == eMotionMode.ABS) {
|
|
_Scanner.JumpAbs(xPos, yPos);
|
|
} else {
|
|
_Scanner.JumpRel(xPos, yPos);
|
|
}
|
|
|
|
//Jump 후 Speed 원복
|
|
_Scanner.SetJumpSpeedCtrl(_ScannerConfig.JumpSpeed);
|
|
}
|
|
|
|
private bool CheckOverLimitPos(double calcPosX, double calcPosY)
|
|
{
|
|
if (calcPosX < CommandPosLimitMinX || calcPosX > CommandPosLimitMaxX) {
|
|
return true;
|
|
} else if (calcPosY < CommandPosLimitMinY || calcPosY > CommandPosLimitMaxY) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|