feat: 레시피 화면에서 스캐너 카메라+모션 토글 창 추가 (이벤트 구동)
- 레시피 컨트롤박스에 '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>
This commit is contained in:
parent
020840e678
commit
4ed35cb94a
@ -144,6 +144,13 @@ namespace WeldingCommon
|
||||
public const string EVENT_CHANGE_DOOR_LOCK = "EventChangeDoorLock";
|
||||
public const string EVENT_CHANGE_RECIPE_INFO = "EventChangeRecipeInfo";
|
||||
public const string EVENT_AUTO_RUN_UPDATE_PAGENUM = "EventAutoRunUpdatePageNum";
|
||||
|
||||
// 레시피 화면(publish) → 매뉴얼 스캐너 VM(subscribe)로 스캐너 모션 요청을 전달한다.
|
||||
// Param: WeldingCommon.Views.ScannerCommandMoveParam
|
||||
// 주의: 구독자(UCScannerMotionViewModel, execute 모드)는 RegisterViews에서 기동 시 1회 생성되어
|
||||
// 프로세스 내내 생존하는 전제(EquipmentSetup.RegisterViews)에 의존한다. View 생성을
|
||||
// 지연 생성으로 바꾸면 구독자가 없어 본 이벤트가 무시될 수 있으므로 함께 점검할 것.
|
||||
public const string EVENT_SCANNER_COMMAND_MOVE = "EventScannerCommandMove";
|
||||
}
|
||||
|
||||
public class CommonSequenceIds
|
||||
|
||||
@ -11,6 +11,7 @@ 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;
|
||||
|
||||
@ -34,6 +35,29 @@ namespace WeldingCommon.Views
|
||||
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");
|
||||
@ -80,12 +104,17 @@ namespace WeldingCommon.Views
|
||||
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)
|
||||
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;
|
||||
@ -99,11 +128,52 @@ namespace WeldingCommon.Views
|
||||
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;
|
||||
@ -190,49 +260,56 @@ namespace WeldingCommon.Views
|
||||
{
|
||||
// TODO: Left, Right, Up, Down 은 장비 Scanner 의 방향에 따라 달라질 수 있다
|
||||
// 양산중에는 동작 하면 안됨
|
||||
var MainSequence = _Aurora.SequenceEntry[CommonSequenceIds.SeqAutoRun].RunState;
|
||||
if(MainSequence == SequenceRunState.Running)
|
||||
if (IsMainSequenceRunning())
|
||||
return;
|
||||
|
||||
eAxisType axis;
|
||||
eJogDirection direction;
|
||||
switch ((string)commandParameter) {
|
||||
case "Left": {
|
||||
MoveJogXYPos(eAxisType.AXIS_X, eJogDirection.MINUS);
|
||||
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);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
|
||||
case "Right": {
|
||||
MoveJogXYPos(eAxisType.AXIS_X, eJogDirection.PLUS);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Up": {
|
||||
MoveJogXYPos(eAxisType.AXIS_Y, eJogDirection.PLUS);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Down": {
|
||||
MoveJogXYPos(eAxisType.AXIS_Y, eJogDirection.MINUS);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Home": {
|
||||
MoveJogHome();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
private void MoveJogXYPos(eAxisType axisType, eJogDirection direction)
|
||||
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;
|
||||
xPos = stepValue * -1;
|
||||
yPos = 0;
|
||||
} else if (direction == eJogDirection.PLUS) {
|
||||
xPos = StepValue;
|
||||
xPos = stepValue;
|
||||
yPos = 0;
|
||||
}
|
||||
}
|
||||
@ -241,10 +318,10 @@ namespace WeldingCommon.Views
|
||||
case eAxisType.AXIS_Y: {
|
||||
if (direction == eJogDirection.MINUS) {
|
||||
xPos = 0;
|
||||
yPos = StepValue * -1;
|
||||
yPos = stepValue * -1;
|
||||
} else if (direction == eJogDirection.PLUS) {
|
||||
xPos = 0;
|
||||
yPos = StepValue;
|
||||
yPos = stepValue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -259,7 +336,7 @@ namespace WeldingCommon.Views
|
||||
return;
|
||||
}
|
||||
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * _VelocityRate;
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
||||
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
||||
_Scanner.JumpRel(xPos, yPos);
|
||||
|
||||
@ -267,9 +344,9 @@ namespace WeldingCommon.Views
|
||||
_Scanner.SetJumpSpeedCtrl(_ScannerConfig.JumpSpeed);
|
||||
}
|
||||
|
||||
private void MoveJogHome()
|
||||
private void MoveJogHome(double velocityRate)
|
||||
{
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * _VelocityRate;
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
||||
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
||||
_Scanner.JumpAbs(0, 0);
|
||||
|
||||
@ -279,28 +356,37 @@ namespace WeldingCommon.Views
|
||||
|
||||
private void PerformCmdCommandPosMove(object commandParameter)
|
||||
{
|
||||
eAxisType axis;
|
||||
switch ((string)commandParameter) {
|
||||
case "X": {
|
||||
MoveCommandXYPos(eAxisType.AXIS_X);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Y": {
|
||||
MoveCommandXYPos(eAxisType.AXIS_Y);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "X": axis = eAxisType.AXIS_X; break;
|
||||
case "Y": axis = eAxisType.AXIS_Y; break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
private void MoveCommandXYPos(eAxisType axisType)
|
||||
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;
|
||||
xPos = commandPosX;
|
||||
|
||||
if (_MotionMode == eMotionMode.ABS) {
|
||||
if (motionMode == eMotionMode.ABS) {
|
||||
yPos = _Scanner.GetCommandPosY();
|
||||
} else {
|
||||
yPos = 0.0;
|
||||
@ -309,13 +395,13 @@ namespace WeldingCommon.Views
|
||||
break;
|
||||
|
||||
case eAxisType.AXIS_Y: {
|
||||
if (_MotionMode == eMotionMode.ABS) {
|
||||
if (motionMode == eMotionMode.ABS) {
|
||||
xPos = _Scanner.GetCommandPosX();
|
||||
} else {
|
||||
xPos = 0.0;
|
||||
}
|
||||
|
||||
yPos = CommandPosY;
|
||||
yPos = commandPosY;
|
||||
}
|
||||
break;
|
||||
|
||||
@ -324,7 +410,7 @@ namespace WeldingCommon.Views
|
||||
}
|
||||
|
||||
// 현재 Curruent Pos 기준으로, 입력방향으로 입력값이 Limit 를 넘어 서는지 확인
|
||||
if (_MotionMode == eMotionMode.ABS) {
|
||||
if (motionMode == eMotionMode.ABS) {
|
||||
if (CheckOverLimitPos(xPos, yPos)) {
|
||||
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
||||
return;
|
||||
@ -336,10 +422,10 @@ namespace WeldingCommon.Views
|
||||
}
|
||||
}
|
||||
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * _VelocityRate;
|
||||
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
||||
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
||||
|
||||
if (_MotionMode == eMotionMode.ABS) {
|
||||
if (motionMode == eMotionMode.ABS) {
|
||||
_Scanner.JumpAbs(xPos, yPos);
|
||||
} else {
|
||||
_Scanner.JumpRel(xPos, yPos);
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
<RowDefinition Height="auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GroupBox Header="Scout Image" Grid.Row="0" Margin="0 3 0 0" Style="{StaticResource StGroupBox2nd}">
|
||||
@ -153,7 +154,14 @@
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Import Object From DXF" Grid.Row="2" Margin="0 5 0 0" Style="{StaticResource StGroupBox2nd}">
|
||||
<Button Grid.Row="2" Margin="0 5 0 0" HorizontalAlignment="Right" Padding="8 3" Command="{Binding CMD_ToggleScannerCamView}" Style="{StaticResource StButtonAccentBlue}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Width="20" Height="10" Margin="5 0 5 0" Source="../../Resource/guide.png"/>
|
||||
<TextBlock Text="Scanner Cam"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<GroupBox Header="Import Object From DXF" Grid.Row="3" Margin="0 5 0 0" Style="{StaticResource StGroupBox2nd}">
|
||||
<Button Margin="5" Command="{Binding CMD_ImportObjectFromDXF}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" Style="{StaticResource StButtonAccentBlue}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Image Width="20" Height="10" Margin="5 0 5 0" Source="../../Resource/dxf.png"/>
|
||||
|
||||
@ -44,6 +44,7 @@ namespace WeldingCommon.Views
|
||||
public ICommand CMD_LaserGuideBeam { get; }
|
||||
public ICommand CMD_LaserReadyCheck { get; }
|
||||
public ICommand CMD_ImportObjectFromDXF { get; }
|
||||
public ICommand CMD_ToggleScannerCamView { get; }
|
||||
|
||||
private readonly IAuroraEnvironment _Aurora;
|
||||
private SystemParameter _SystemParameter;
|
||||
@ -102,6 +103,9 @@ namespace WeldingCommon.Views
|
||||
|
||||
private SequenceDialog _SequenceDialog;
|
||||
|
||||
// 레시피 화면에서 토글로 띄우는 스캐너 카메라 + 모션 창 (재사용을 위해 Hide/Show)
|
||||
private WDScannerCamMotion _ScannerCamWindow;
|
||||
|
||||
private WinSequenceDiagram _WinSequenceDiagramScoutNormal;
|
||||
private WinSequenceDiagram _WinSequenceDiagramScoutFlyingVision;
|
||||
private WinSequenceDiagram _WinSequenceDiagramTeachingRun;
|
||||
@ -139,6 +143,7 @@ namespace WeldingCommon.Views
|
||||
CMD_LaserGuideBeam = new DelegateCommand(PerformCmdLaserGuideBeam);
|
||||
CMD_LaserReadyCheck = new DelegateCommand(PerformCmdLaserReadyCheck);
|
||||
CMD_ImportObjectFromDXF = new DelegateCommand(PerformCmdImportObjectFromDXF);
|
||||
CMD_ToggleScannerCamView = new DelegateCommand(PerformCmdToggleScannerCamView);
|
||||
|
||||
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_SETTING_CHANGED)
|
||||
.ObserveOnDispatcher()
|
||||
@ -511,6 +516,22 @@ namespace WeldingCommon.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformCmdToggleScannerCamView(object obj)
|
||||
{
|
||||
if (_ScannerCamWindow == null) {
|
||||
_ScannerCamWindow = new WDScannerCamMotion(_Aurora, CommonDeviceIds.Scanner) {
|
||||
Owner = Application.Current.MainWindow,
|
||||
};
|
||||
}
|
||||
|
||||
if (_ScannerCamWindow.IsVisible) {
|
||||
_ScannerCamWindow.Hide();
|
||||
} else {
|
||||
_ScannerCamWindow.Show();
|
||||
_ScannerCamWindow.Activate();
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformCmdImportObjectFromDXF(object obj)
|
||||
{
|
||||
System.Windows.Forms.FileDialog dlg = new System.Windows.Forms.OpenFileDialog();
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<Window x:Class="WeldingCommon.Views.WDScannerCamMotion"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:WeldingCommon.Views"
|
||||
mc:Ignorable="d"
|
||||
Name="uc"
|
||||
Background="#FF2D2D30"
|
||||
Width="1100" Height="700"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Title="Scanner Cam / Motion"
|
||||
IsVisibleChanged="Window_IsVisibleChanged">
|
||||
<Grid Margin="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1.3*"/>
|
||||
<ColumnDefinition Width="1*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 카메라 라이브 뷰 (전용 displayType: Machine_C) -->
|
||||
<Grid Grid.Column="0" Margin="5" x:Name="gridScannerCameraView"/>
|
||||
<!-- 매뉴얼 스캐너 모션 컨트롤 재사용 (publish 모드 VM 주입) -->
|
||||
<local:UCScannerMotion Grid.Column="1" Margin="5" x:Name="motionView"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@ -0,0 +1,64 @@
|
||||
using Aurora;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using VisionCommon;
|
||||
using VisionUI;
|
||||
using VisionUI.Camera;
|
||||
|
||||
namespace WeldingCommon.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 레시피 화면에서 토글로 띄우는 "스캐너 카메라 + 모션" 창.
|
||||
/// - 카메라: 매뉴얼 화면(Machine_B)과 충돌하지 않도록 전용 displayType(Machine_C)로 별도 인스턴스 생성.
|
||||
/// - 모션: 매뉴얼 UCScannerMotion 을 그대로 재사용하되, publish 모드 VM 을 주입해
|
||||
/// 버튼 입력값을 이벤트(EVENT_SCANNER_COMMAND_MOVE)로 발행 → 매뉴얼 측 VM 이 실제 이동 수행.
|
||||
/// - 닫기: Dispose 하지 않고 Hide → 언제든 다시 Show 가능.
|
||||
/// </summary>
|
||||
public partial class WDScannerCamMotion : Window
|
||||
{
|
||||
private const eDisplayType DisplayType = eDisplayType.Machine_C;
|
||||
|
||||
private delegate void HideWindow();
|
||||
|
||||
public WDScannerCamMotion(IAuroraEnvironment aurora, string scannerId)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
motionView.DataContext = new UCScannerMotionViewModel(aurora, scannerId, publishMode: true);
|
||||
|
||||
CreateScannerCameraView();
|
||||
}
|
||||
|
||||
private void CreateScannerCameraView()
|
||||
{
|
||||
if (gridScannerCameraView.Children.Count == 0) {
|
||||
var fovInfo = VisionRecipeManager.GetFovInfos().FirstOrDefault();
|
||||
if (fovInfo != null) {
|
||||
var cameraView = IpViewManager.CreateCameraView(fovInfo, DisplayType);
|
||||
gridScannerCameraView.Children.Add(cameraView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
bool isVisible = (bool)e.NewValue;
|
||||
if (isVisible) {
|
||||
CameraUcManager.Instance.StartLiveAll(DisplayType);
|
||||
} else {
|
||||
CameraUcManager.Instance.StopLiveAll();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosing(CancelEventArgs e)
|
||||
{
|
||||
// 닫기 버튼/Alt+F4 시에도 파괴하지 않고 숨김 처리(재사용 유지).
|
||||
CameraUcManager.Instance.StopLiveAll();
|
||||
e.Cancel = true;
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new HideWindow(() => this.Hide()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -385,6 +385,9 @@
|
||||
</Compile>
|
||||
<Compile Include="Views\Recipes\UCScannerObjectTeachingControlBoxViewModel.cs" />
|
||||
<Compile Include="Views\Recipes\UCScannerObjectTeachingViewModel.cs" />
|
||||
<Compile Include="Views\Recipes\WDScannerCamMotion.xaml.cs">
|
||||
<DependentUpon>WDScannerCamMotion.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\Test\UCTestDevice.xaml.cs">
|
||||
<DependentUpon>UCTestDevice.xaml</DependentUpon>
|
||||
</Compile>
|
||||
@ -704,6 +707,10 @@
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Recipes\WDScannerCamMotion.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Views\Test\UCTestDevice.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@ -0,0 +1,186 @@
|
||||
# 레시피 화면에서 기존 스캐너 모션 + 카메라 뷰 재사용 가능성 분석
|
||||
|
||||
> 작성 2026-06-23. 목적: 레시피 티칭 화면(`UCScannerObjectTeaching`)에서
|
||||
> **기존 매뉴얼 화면의 스캐너 모션 컨트롤(`UCScannerMotion`) + 카메라 라이브 뷰(`CameraView`)** 를
|
||||
> 재사용해, "스캐너를 원하는 위치로 이동시켜 카메라로 보면서 티칭"하는 워크플로를
|
||||
> 한 화면에서 돌릴 수 있는지 판정한다.
|
||||
> **이 문서는 분석 전용 — 소스 변경 없음.**
|
||||
|
||||
---
|
||||
|
||||
## 0. 핵심 결론 (먼저)
|
||||
|
||||
| 재사용 대상 | 재사용 가능성 | 핵심 제약 |
|
||||
|---|---|---|
|
||||
| **스캐너 모션 컨트롤** `UCScannerMotion` (+VM) | ✅ **그대로 재사용 가능** | 생성 시 `scannerId` 주입 필요 / 모니터링 타이머 On·Off |
|
||||
| **카메라 라이브 뷰** `CameraView` | ✅ 가능 (별도 인스턴스) | WPF 단일 부모 / `displayType` 전역 등록 충돌 |
|
||||
|
||||
→ **둘 다 재사용 가능**. 단, "복붙"이 아니라 **VM을 새로 물려주고(또는 공유) toggle Window/패널로 띄우는** 방식이어야 안전하다. 자세한 권장안은 6장.
|
||||
|
||||
---
|
||||
|
||||
## 1. 요구 워크플로
|
||||
|
||||
레시피 티칭 화면에서:
|
||||
1. 작은 버튼으로 **CAM 라이브 뷰**를 띄운다 (언제든 띄웠다 닫았다).
|
||||
2. `CommandPosX/Y` 입력 + `Move X`/`Move Y`(`CMD_CommandPosMove`, 파라미터 `"X"`/`"Y"`)로
|
||||
스캐너를 명령 위치로 이동.
|
||||
3. 이동 결과를 카메라로 눈으로 확인하며 객체 위치를 티칭.
|
||||
|
||||
즉 **이동(모션) + 관찰(카메라)** 두 기능이 한 화면에 필요하다.
|
||||
카메라만 띄우면 *보기만* 되고 *움직이기*는 안 된다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 현재 구조 (출처 파일)
|
||||
|
||||
### 2.1 매뉴얼 스캐너 화면 (재사용 원본)
|
||||
- `Views/Manual/Scanner/UCManualScannerOperation.xaml(.cs)` — 컨테이너
|
||||
- 좌측: `gridScannerCameraView` 에 `CameraView`(displayType=`Machine_B`) 코드비하인드로 add
|
||||
(`UCManualScannerOperation.xaml.cs:38-47` `CreateScannerCameraView()`)
|
||||
- 가시성 토글 시 `CameraUcManager.Instance.StartLiveAll(Machine_B)` / `StopLiveAll()`
|
||||
(`UCManualScannerOperation.xaml.cs:49-57`)
|
||||
- 우측 상단: `UCScannerMotion` (`UCManualScannerOperation.xaml:59`)
|
||||
- VM: `UCManualScannerOperationViewModel`
|
||||
- `ScannerMotionViewModel = new UCScannerMotionViewModel(_Aurora, scannerId)`
|
||||
(`UCManualScannerOperationViewModel.cs:40`)
|
||||
- 생성자 시그니처가 `(aurora, scannerId, isUseMarkingOntheFly)` — **scannerId를 외부에서 주입받음**
|
||||
|
||||
### 2.2 스캐너 모션 컨트롤 (재사용 핵심)
|
||||
- `Views/Manual/Scanner/UCScannerMotion.xaml(.cs)` + `UCScannerMotionViewModel.cs`
|
||||
- 보유 기능 전부 VM 자체에 캡슐화되어 있음:
|
||||
- `CommandPosX/Y`, `Move X/Y` → `CMD_CommandPosMove("X"/"Y")` → `MoveCommandXYPos()`
|
||||
→ `_Scanner.JumpAbs/JumpRel` (`UCScannerMotionViewModel.cs:280-350`)
|
||||
- Jog(▲▼◀▶/Home), Step, Speed(Low/Mid/High), Mode(ABS/REL)
|
||||
- X/Y Limit 체크(`CheckOverLimitPos`), 현재좌표/Busy 모니터링(100ms DispatcherTimer)
|
||||
- **안전장치 내장**: 양산(`SeqAutoRun.Running`) 중 Jog 차단(`:193-195`), Limit 초과 차단
|
||||
- 의존성: `IAuroraEnvironment` + `scannerId` 뿐. `_Scanner`, `_Laser`, `_ScannerConfig`를
|
||||
`DeviceManager`에서 꺼냄(`:90-92`). → **레시피 VM 환경에서도 동일하게 확보 가능.**
|
||||
|
||||
### 2.3 카메라 라이브 뷰
|
||||
- `IpViewManager.CreateCameraView(fovInfo, displayType)` 가 `new CameraView` 반환(`IpViewManager.cs:34-37`)
|
||||
- `CameraView` 생성자가 전역 등록 수행(`MilUI/Camera/CameraView.xaml.cs:58-59`):
|
||||
```csharp
|
||||
CameraViewManager.Instance.SetCameraView(displayType, fovInfo, this);
|
||||
CameraDisplayManager.SetDisplay(displayType, fovInfo, milCameraDisplay);
|
||||
```
|
||||
- `CameraDisplayManager`는 `(displayType, fovInfo)`당 **디스플레이 1개만 보관**(`CameraDisplayManager.cs:23-25, 32-48`)
|
||||
- 라이브 On/Off: `CameraUcManager.Instance.StartLiveAll(displayType)` / `StopLiveAll()`
|
||||
(`CameraUcManager.cs:134-157`) — 내부적으로 `GetDisplay(fovInfo, displayType)`로 디스플레이를 찾음
|
||||
|
||||
### 2.4 레시피 티칭 화면 (재사용 대상지)
|
||||
- `Views/Recipes/UCScannerObjectTeaching.xaml(.cs)` + `...ViewModel.cs`
|
||||
- 좌측 300px: `UCScannerObjectTeachingControlBox`
|
||||
- 우측: `WindowsFormsHost` → LASA `MainPage` (WinForms 티칭 화면)
|
||||
- 그리드 전체 `IsEnabled="{Binding IsManualStatus}"` (매뉴얼 모드 게이팅)
|
||||
- VM: `UCScannerObjectTeachingViewModel(IAuroraEnvironment aurora)` — **현재 scannerId 안 받음**
|
||||
- `IsManualStatus`를 `EVENT_EQUIPMENT_STATUS_CHANGE`로 갱신(`:75-80`) → 모션 게이팅에 그대로 활용 가능
|
||||
|
||||
---
|
||||
|
||||
## 3. 재사용 가능성 판정
|
||||
|
||||
### 3.1 스캐너 모션 컨트롤 — ✅ 그대로 재사용 가능
|
||||
- `UCScannerMotion`은 **UI/로직이 VM에 완결**되어 있고 외부 의존이 `aurora + scannerId` 뿐이라
|
||||
레시피 화면에 `UCScannerMotion`을 그대로 배치하고 DataContext에
|
||||
`new UCScannerMotionViewModel(aurora, scannerId)`만 물려주면 동작한다.
|
||||
- 안전 인터락(양산 중 Jog 차단, Limit 체크)이 VM에 내장 → 레시피 화면에서 써도 동일하게 보호됨.
|
||||
- **확보해야 할 것: `scannerId`**
|
||||
- 매뉴얼 화면은 등록 시점(`EquipmentSetup`)에서 scannerId를 받아 넘긴다.
|
||||
- 레시피 VM은 현재 안 받으므로, ① 레시피 VM 생성자에 scannerId 파라미터 추가하거나
|
||||
② `DeviceManager`에서 Scanner 디바이스 id를 조회해 얻으면 된다(매뉴얼과 동일 값 사용).
|
||||
|
||||
### 3.2 카메라 라이브 뷰 — ✅ 가능(단, 별도 인스턴스 + 전용 displayType)
|
||||
- **제약 A (WPF 단일 부모):** `gridScannerCameraView`에 들어간 매뉴얼 화면의 `CameraView`
|
||||
인스턴스를 레시피 화면에 동시에 붙일 수 없다(UIElement 부모는 1개). → 레시피용으로
|
||||
`CreateCameraView`를 다시 호출해 **새 인스턴스**를 만들어야 한다.
|
||||
- **제약 B (displayType 전역 등록 충돌):** 새 인스턴스를 매뉴얼과 같은 `Machine_B`로 만들면
|
||||
생성자의 `CameraDisplayManager.SetDisplay(Machine_B, fov, ...)`가 매뉴얼 등록을 **덮어쓴다**.
|
||||
이후 매뉴얼 화면의 `StartLiveAll(Machine_B)`가 엉뚱한 디스플레이를 잡을 위험.
|
||||
→ 레시피 팝업은 **전용 displayType**을 써야 한다.
|
||||
- `eDisplayType` enum에 `Machine_C`가 **정의만 되고 현재 미사용**(`CamCommon.cs:22`, 전 코드 검색상 참조 0건)
|
||||
→ 레시피 팝업 전용으로 적합.
|
||||
- **라이브 On/Off:** 매뉴얼 화면처럼 띄울 때 `StartLiveAll(Machine_C)`, 닫을 때 `StopLiveAll()`을
|
||||
**직접 호출**해야 한다(자동 아님).
|
||||
|
||||
---
|
||||
|
||||
## 4. 리스크 / 주의
|
||||
|
||||
| # | 리스크 | 완화 |
|
||||
|---|---|---|
|
||||
| R1 | 같은 `Machine_B`로 카메라뷰 2개 생성 시 디스플레이 등록 덮어쓰기 | 레시피는 `Machine_C` 전용 사용 |
|
||||
| R2 | 같은 CameraView 인스턴스 2화면 공유 시도(부모 충돌 예외) | 인스턴스 분리 생성 |
|
||||
| R3 | 모션 VM 2개(매뉴얼/레시피)가 같은 `_Scanner`를 동시 제어 | 둘은 다른 탭이라 동시 표시 안 됨 + Busy/양산 인터락 내장. 단 VM 공유로도 회피 가능(6.C) |
|
||||
| R4 | 모니터링 타이머 누수 | `UCScannerMotionViewModel`은 `CMD_VisibleChanged`로 타이머 Start/Stop(`:113-122`). 레시피에서도 `IsVisibleChanged`/패널 토글에 연결 필요 |
|
||||
| R5 | scannerId 불일치 | 매뉴얼과 동일한 scannerId 사용(DeviceManager 조회 또는 동일 주입) |
|
||||
| R6 | 카메라뷰 Dispose 누락으로 자원 점유 | 화면 종료/탭 이탈 시 `IpViewManager.DisposeCameraView` 호출 권장 |
|
||||
|
||||
LASA `MainPage`(WindowsFormsHost)와 WPF 카메라 팝업의 공존은 **별도 top-level Window**로
|
||||
띄우면 airspace 문제 없음. 패널 인라인(같은 WPF 트리) 방식도 가능.
|
||||
|
||||
---
|
||||
|
||||
## 5. scannerId 확보 경로 (참고)
|
||||
|
||||
- 매뉴얼 경로: `EquipmentSetup` 등록 → `UCManualScannerOperationViewModel(aurora, scannerId, ...)`
|
||||
→ `UCScannerMotionViewModel(aurora, scannerId)`.
|
||||
- 레시피 경로(택1):
|
||||
1. 레시피 VM 생성자에 `scannerId` 추가(등록부에서 매뉴얼과 동일 값 전달) — 명시적·안전
|
||||
2. `aurora.DeviceManager`에서 Scanner 타입 디바이스 id 조회 — 등록부 변경 불필요
|
||||
|
||||
---
|
||||
|
||||
## 6. 권장 구현안 (셋 중 택1)
|
||||
|
||||
### A. 카메라만 (최소, 위험 최저)
|
||||
- 레시피 컨트롤박스에 작은 버튼 → 전용 toggle Window에 `CameraView(Machine_C)` 1회 생성·캐시,
|
||||
Show/Hide. Show 시 `StartLiveAll(Machine_C)`, Hide/Close 시 `StopLiveAll()`.
|
||||
- Close는 `e.Cancel=true; Hide()` 패턴(프레임워크 `CameraViewWindow.OnClosing`과 동일)으로 재사용 유지.
|
||||
- 한계: 이동은 매뉴얼 화면에서 따로 해야 함. "보기 전용".
|
||||
|
||||
### B. 카메라 + 모션을 한 팝업에 (요구 워크플로 완성) ← 추천
|
||||
- toggle Window 안에 `UCScannerMotion`(DataContext=`new UCScannerMotionViewModel(aurora, scannerId)`)
|
||||
+ `CameraView(Machine_C)`를 좌우로 배치.
|
||||
- Window 가시성에 모션 VM의 `CMD_VisibleChanged`(타이머)와 카메라 라이브 On/Off를 함께 연결.
|
||||
- 이 한 곳에서 이동→관찰 루프 완성. R3는 탭 분리 + 인터락으로 사실상 무해.
|
||||
|
||||
### C. 모션 VM 공유(인라인 패널)
|
||||
- 레시피 VM이 `UCScannerMotionViewModel`을 프로퍼티로 보유(매뉴얼과 동일 인스턴스 패턴),
|
||||
레시피 화면에 `UCScannerMotion`을 인라인 배치 + 카메라는 별 패널/팝업.
|
||||
- 장점: 모션 제어 주체가 명확(중복 제어 원천 차단). 단 레시피 VM 변경 폭이 가장 큼.
|
||||
|
||||
---
|
||||
|
||||
### D. 이벤트 구동 — 모션 로직 재사용 (모션 부분 최선) ← 채택 검토
|
||||
> 사용자 제안. "레시피엔 입력+버튼만, 누르면 Aurora 이벤트 발행 → 구독 중인 매뉴얼
|
||||
> `UCScannerMotionViewModel`이 같은 메서드를 호출"한다.
|
||||
|
||||
- **성립 근거(검증됨):** 매뉴얼 스캐너 View/VM은 `EquipmentSetup.RegisterViews()`에서
|
||||
**앱 기동 시 1회 즉시 생성**되어 프로세스 내내 생존
|
||||
(`Machines/ILSOS_PrismaticRD_PTOP/EquipmentSetup.cs:290`).
|
||||
→ 레시피 탭에 있어도 매뉴얼 VM의 이벤트 구독이 살아있어 발행이 도달함.
|
||||
Aurora `EventObservable`/`EventPublisher`는 전역 pub/sub(레시피 VM도 이미 다수 구독, `...ViewModel.cs:55-69`).
|
||||
- **장점:** 모션 로직(Move/Limit/양산중 차단 인터락) 무중복 재사용, 레시피 측은 입력+버튼+발행만.
|
||||
직접 참조 없이 느슨한 결합.
|
||||
- **반드시 보완할 3가지:**
|
||||
1. **페이로드 필수.** 현재 `MoveCommandXYPos()`는 `CommandPosX/Y`·`_MotionMode`·`_VelocityRate`를
|
||||
**매뉴얼 VM 자기 state에서** 읽음(`UCScannerMotionViewModel.cs:301,303,339`).
|
||||
레시피에서 이벤트만 쏘면 매뉴얼 VM의 옛 값으로 움직임 → 이벤트 `Param`에
|
||||
`{axis, posX, posY, mode, speed}`를 담고, 매뉴얼 측은 **인자형 오버로드**
|
||||
`MoveCommandXYPos(axis, x, y, mode, speed)`를 호출(기존 `CMD_CommandPosMove`는 UI 전용 유지).
|
||||
2. **스레드.** 구독은 UI 스레드 보장 없음 → 매뉴얼 측 구독에 `.ObserveOnDispatcher()` 권장.
|
||||
3. **숨은 결합.** "매뉴얼 VM 항상 생존"(즉시생성)에 의존. 추후 지연 생성 전환 시 레시피 버튼이
|
||||
조용히 무반응. → 이벤트 상수 정의부 주석으로 의존 명시 + 무응답 로그 가드.
|
||||
- **범위 한계:** 이벤트 트릭은 **모션 한정**. **카메라 뷰는 별개**(옵션 A 방식: `Machine_C` 전용
|
||||
인스턴스 + 라이브 On/Off 직접 구동).
|
||||
|
||||
## 7. 최종 판단
|
||||
|
||||
- **기술적으로 전면 재사용 가능.**
|
||||
- **모션은 옵션 D(이벤트 구동) 채택 검토** — 매뉴얼 VM이 즉시생성으로 항상 생존함이 확인되어
|
||||
성립하며, scannerId 주입이나 모션 VM 중복 없이 로직을 그대로 재사용한다.
|
||||
(3가지 보완: 이벤트 페이로드, `ObserveOnDispatcher`, 숨은 결합 가드)
|
||||
- **카메라는 옵션 A 방식** — 전용 `Machine_C`로 새 인스턴스 생성 + 라이브 On/Off 직접 구동.
|
||||
- 후속 작업: ① 이벤트 상수 + 페이로드 타입 정의 → ② 매뉴얼 측 인자형 오버로드 분리 + 구독 추가
|
||||
→ ③ 레시피 측 입력/버튼/발행 + 카메라 토글(Machine_C) 구현.
|
||||
Loading…
x
Reference in New Issue
Block a user