Compare commits
5 Commits
2871db6e8f
...
9f72e6caaf
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f72e6caaf | |||
| a64dd12519 | |||
| d272d6e7e9 | |||
| 357702399c | |||
| 4ed35cb94a |
@ -144,6 +144,13 @@ namespace WeldingCommon
|
|||||||
public const string EVENT_CHANGE_DOOR_LOCK = "EventChangeDoorLock";
|
public const string EVENT_CHANGE_DOOR_LOCK = "EventChangeDoorLock";
|
||||||
public const string EVENT_CHANGE_RECIPE_INFO = "EventChangeRecipeInfo";
|
public const string EVENT_CHANGE_RECIPE_INFO = "EventChangeRecipeInfo";
|
||||||
public const string EVENT_AUTO_RUN_UPDATE_PAGENUM = "EventAutoRunUpdatePageNum";
|
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
|
public class CommonSequenceIds
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
|
|
||||||
@ -34,6 +35,29 @@ namespace WeldingCommon.Views
|
|||||||
REL
|
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
|
public class UCScannerMotionViewModel : NotifyPropertyChangedBase
|
||||||
{
|
{
|
||||||
private readonly Logger Log = new Logger("UCScannerMotionViewModel");
|
private readonly Logger Log = new Logger("UCScannerMotionViewModel");
|
||||||
@ -80,12 +104,17 @@ namespace WeldingCommon.Views
|
|||||||
private double _VelocityRate;
|
private double _VelocityRate;
|
||||||
private eMotionMode _MotionMode;
|
private eMotionMode _MotionMode;
|
||||||
|
|
||||||
|
// publish 모드: UI(레시피)는 직접 디바이스를 제어하지 않고, 입력값을 이벤트로 발행한다.
|
||||||
|
// execute 모드(기본, 매뉴얼): 이벤트를 구독해 실제 스캐너를 움직인다.
|
||||||
|
private readonly bool _PublishMode;
|
||||||
|
|
||||||
private readonly DispatcherTimer _MonitorTimer;
|
private readonly DispatcherTimer _MonitorTimer;
|
||||||
|
|
||||||
// PAM, RUW 처럼 2개 이상의 scanner 를 사용하는 경우가 있어, scannerId 를 받는 형태로 생성
|
// PAM, RUW 처럼 2개 이상의 scanner 를 사용하는 경우가 있어, scannerId 를 받는 형태로 생성
|
||||||
public UCScannerMotionViewModel(IAuroraEnvironment aurora, string scannerId)
|
public UCScannerMotionViewModel(IAuroraEnvironment aurora, string scannerId, bool publishMode = false)
|
||||||
{
|
{
|
||||||
_Aurora = aurora;
|
_Aurora = aurora;
|
||||||
|
_PublishMode = publishMode;
|
||||||
|
|
||||||
_Scanner = _Aurora.DeviceManager[scannerId] as Scanner;
|
_Scanner = _Aurora.DeviceManager[scannerId] as Scanner;
|
||||||
_Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as Laser;
|
_Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as Laser;
|
||||||
@ -99,11 +128,52 @@ namespace WeldingCommon.Views
|
|||||||
CMD_JogMove = new DelegateCommand(PerformCmdJogMove);
|
CMD_JogMove = new DelegateCommand(PerformCmdJogMove);
|
||||||
CMD_CommandPosMove = new DelegateCommand(PerformCmdCommandPosMove);
|
CMD_CommandPosMove = new DelegateCommand(PerformCmdCommandPosMove);
|
||||||
|
|
||||||
|
// execute 모드만 원격 모션 요청을 구독한다(중복 실행 방지). UI 스레드에서 처리.
|
||||||
|
if (!_PublishMode) {
|
||||||
|
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_SCANNER_COMMAND_MOVE)
|
||||||
|
.ObserveOnDispatcher()
|
||||||
|
.Subscribe(OnRemoteMotionRequested);
|
||||||
|
}
|
||||||
|
|
||||||
_MonitorTimer = new DispatcherTimer();
|
_MonitorTimer = new DispatcherTimer();
|
||||||
_MonitorTimer.Tick += OnMonitoring;
|
_MonitorTimer.Tick += OnMonitoring;
|
||||||
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100);
|
_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()
|
private void InitVariable()
|
||||||
{
|
{
|
||||||
_VelocityRate = 1;
|
_VelocityRate = 1;
|
||||||
@ -190,49 +260,56 @@ namespace WeldingCommon.Views
|
|||||||
{
|
{
|
||||||
// TODO: Left, Right, Up, Down 은 장비 Scanner 의 방향에 따라 달라질 수 있다
|
// TODO: Left, Right, Up, Down 은 장비 Scanner 의 방향에 따라 달라질 수 있다
|
||||||
// 양산중에는 동작 하면 안됨
|
// 양산중에는 동작 하면 안됨
|
||||||
var MainSequence = _Aurora.SequenceEntry[CommonSequenceIds.SeqAutoRun].RunState;
|
if (IsMainSequenceRunning())
|
||||||
if(MainSequence == SequenceRunState.Running)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
eAxisType axis;
|
||||||
|
eJogDirection direction;
|
||||||
switch ((string)commandParameter) {
|
switch ((string)commandParameter) {
|
||||||
case "Left": {
|
case "Left": axis = eAxisType.AXIS_X; direction = eJogDirection.MINUS; break;
|
||||||
MoveJogXYPos(eAxisType.AXIS_X, eJogDirection.MINUS);
|
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": {
|
default:
|
||||||
MoveJogXYPos(eAxisType.AXIS_X, eJogDirection.PLUS);
|
return;
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Up": {
|
|
||||||
MoveJogXYPos(eAxisType.AXIS_Y, eJogDirection.PLUS);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Down": {
|
|
||||||
MoveJogXYPos(eAxisType.AXIS_Y, eJogDirection.MINUS);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Home": {
|
|
||||||
MoveJogHome();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private void MoveJogXYPos(eAxisType axisType, eJogDirection direction, double stepValue, double velocityRate)
|
||||||
{
|
{
|
||||||
double xPos = 0.0;
|
double xPos = 0.0;
|
||||||
double yPos = 0.0;
|
double yPos = 0.0;
|
||||||
switch (axisType) {
|
switch (axisType) {
|
||||||
case eAxisType.AXIS_X: {
|
case eAxisType.AXIS_X: {
|
||||||
if (direction == eJogDirection.MINUS) {
|
if (direction == eJogDirection.MINUS) {
|
||||||
xPos = StepValue * -1;
|
xPos = stepValue * -1;
|
||||||
yPos = 0;
|
yPos = 0;
|
||||||
} else if (direction == eJogDirection.PLUS) {
|
} else if (direction == eJogDirection.PLUS) {
|
||||||
xPos = StepValue;
|
xPos = stepValue;
|
||||||
yPos = 0;
|
yPos = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,10 +318,10 @@ namespace WeldingCommon.Views
|
|||||||
case eAxisType.AXIS_Y: {
|
case eAxisType.AXIS_Y: {
|
||||||
if (direction == eJogDirection.MINUS) {
|
if (direction == eJogDirection.MINUS) {
|
||||||
xPos = 0;
|
xPos = 0;
|
||||||
yPos = StepValue * -1;
|
yPos = stepValue * -1;
|
||||||
} else if (direction == eJogDirection.PLUS) {
|
} else if (direction == eJogDirection.PLUS) {
|
||||||
xPos = 0;
|
xPos = 0;
|
||||||
yPos = StepValue;
|
yPos = stepValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -259,7 +336,7 @@ namespace WeldingCommon.Views
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double finalVelocity = _ScannerConfig.JumpSpeed * _VelocityRate;
|
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
||||||
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
||||||
_Scanner.JumpRel(xPos, yPos);
|
_Scanner.JumpRel(xPos, yPos);
|
||||||
|
|
||||||
@ -267,9 +344,9 @@ namespace WeldingCommon.Views
|
|||||||
_Scanner.SetJumpSpeedCtrl(_ScannerConfig.JumpSpeed);
|
_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.SetJumpSpeedCtrl(finalVelocity);
|
||||||
_Scanner.JumpAbs(0, 0);
|
_Scanner.JumpAbs(0, 0);
|
||||||
|
|
||||||
@ -279,28 +356,37 @@ namespace WeldingCommon.Views
|
|||||||
|
|
||||||
private void PerformCmdCommandPosMove(object commandParameter)
|
private void PerformCmdCommandPosMove(object commandParameter)
|
||||||
{
|
{
|
||||||
|
eAxisType axis;
|
||||||
switch ((string)commandParameter) {
|
switch ((string)commandParameter) {
|
||||||
case "X": {
|
case "X": axis = eAxisType.AXIS_X; break;
|
||||||
MoveCommandXYPos(eAxisType.AXIS_X);
|
case "Y": axis = eAxisType.AXIS_Y; break;
|
||||||
}
|
default: return;
|
||||||
break;
|
|
||||||
|
|
||||||
case "Y": {
|
|
||||||
MoveCommandXYPos(eAxisType.AXIS_Y);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private void MoveCommandXYPos(eAxisType axisType, double commandPosX, double commandPosY, eMotionMode motionMode, double velocityRate)
|
||||||
{
|
{
|
||||||
double xPos = 0.0;
|
double xPos = 0.0;
|
||||||
double yPos = 0.0;
|
double yPos = 0.0;
|
||||||
switch (axisType) {
|
switch (axisType) {
|
||||||
case eAxisType.AXIS_X: {
|
case eAxisType.AXIS_X: {
|
||||||
xPos = CommandPosX;
|
xPos = commandPosX;
|
||||||
|
|
||||||
if (_MotionMode == eMotionMode.ABS) {
|
if (motionMode == eMotionMode.ABS) {
|
||||||
yPos = _Scanner.GetCommandPosY();
|
yPos = _Scanner.GetCommandPosY();
|
||||||
} else {
|
} else {
|
||||||
yPos = 0.0;
|
yPos = 0.0;
|
||||||
@ -309,13 +395,13 @@ namespace WeldingCommon.Views
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case eAxisType.AXIS_Y: {
|
case eAxisType.AXIS_Y: {
|
||||||
if (_MotionMode == eMotionMode.ABS) {
|
if (motionMode == eMotionMode.ABS) {
|
||||||
xPos = _Scanner.GetCommandPosX();
|
xPos = _Scanner.GetCommandPosX();
|
||||||
} else {
|
} else {
|
||||||
xPos = 0.0;
|
xPos = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
yPos = CommandPosY;
|
yPos = commandPosY;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -324,7 +410,7 @@ namespace WeldingCommon.Views
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 현재 Curruent Pos 기준으로, 입력방향으로 입력값이 Limit 를 넘어 서는지 확인
|
// 현재 Curruent Pos 기준으로, 입력방향으로 입력값이 Limit 를 넘어 서는지 확인
|
||||||
if (_MotionMode == eMotionMode.ABS) {
|
if (motionMode == eMotionMode.ABS) {
|
||||||
if (CheckOverLimitPos(xPos, yPos)) {
|
if (CheckOverLimitPos(xPos, yPos)) {
|
||||||
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
Log.E(string.Format("움직이려는 범위가 스캐너 범위를 초과하였습니다."));
|
||||||
return;
|
return;
|
||||||
@ -336,10 +422,10 @@ namespace WeldingCommon.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double finalVelocity = _ScannerConfig.JumpSpeed * _VelocityRate;
|
double finalVelocity = _ScannerConfig.JumpSpeed * velocityRate;
|
||||||
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
_Scanner.SetJumpSpeedCtrl(finalVelocity);
|
||||||
|
|
||||||
if (_MotionMode == eMotionMode.ABS) {
|
if (motionMode == eMotionMode.ABS) {
|
||||||
_Scanner.JumpAbs(xPos, yPos);
|
_Scanner.JumpAbs(xPos, yPos);
|
||||||
} else {
|
} else {
|
||||||
_Scanner.JumpRel(xPos, yPos);
|
_Scanner.JumpRel(xPos, yPos);
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
<RowDefinition Height="auto"></RowDefinition>
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="auto"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<GroupBox Header="Scout Image" Grid.Row="0" Margin="0 3 0 0" Style="{StaticResource StGroupBox2nd}">
|
<GroupBox Header="Scout Image" Grid.Row="0" Margin="0 3 0 0" Style="{StaticResource StGroupBox2nd}">
|
||||||
@ -66,11 +67,18 @@
|
|||||||
|
|
||||||
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Height="50" Margin="0 5 0 0" Visibility="{Binding IsSelectedScoutNormal, Converter={StaticResource convTrueVisible}}">
|
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Height="50" Margin="0 5 0 0" Visibility="{Binding IsSelectedScoutNormal, Converter={StaticResource convTrueVisible}}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="2*"></ColumnDefinition>
|
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="1.3*"></ColumnDefinition>
|
||||||
<ColumnDefinition Width="1*"></ColumnDefinition>
|
<ColumnDefinition Width="1*"></ColumnDefinition>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Button Grid.Column="0" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentPurple}" Command="{Binding CMD_RunScoutNormal}" Content="Run Scout Normal" IsEnabled="{Binding IsSequenceRunning_ScoutNormal, Converter={StaticResource convReverse}}"/>
|
<Button Grid.Column="0" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentBlue}" Command="{Binding CMD_ShowSelectFieldWindow}">
|
||||||
<Button Grid.Column="1" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentPurple}" Command="{Binding CMD_StopScoutNormal}" Content="Stop" IsEnabled="{Binding IsSequenceRunning_ScoutNormal}"/>
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Image Width="20" Height="20" Margin="5 0 5 0" Source="../../Resource/slash.jpg"/>
|
||||||
|
<TextBlock Text="Field"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="1" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentPurple}" Command="{Binding CMD_RunScoutNormal}" Content="Run" IsEnabled="{Binding IsSequenceRunning_ScoutNormal, Converter={StaticResource convReverse}}"/>
|
||||||
|
<Button Grid.Column="2" Margin="3" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" FontWeight="Bold" Style="{StaticResource StButtonAccentPurple}" Command="{Binding CMD_StopScoutNormal}" Content="Stop" IsEnabled="{Binding IsSequenceRunning_ScoutNormal}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Height="50" Margin="0" Visibility="{Binding IsSelectedScoutNormal, Converter={StaticResource convFalseVisible}}">
|
<Grid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Height="50" Margin="0" Visibility="{Binding IsSelectedScoutNormal, Converter={StaticResource convFalseVisible}}">
|
||||||
@ -153,7 +161,14 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</GroupBox>
|
</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}">
|
<Button Margin="5" Command="{Binding CMD_ImportObjectFromDXF}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Height="auto" Style="{StaticResource StButtonAccentBlue}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<Image Width="20" Height="10" Margin="5 0 5 0" Source="../../Resource/dxf.png"/>
|
<Image Width="20" Height="10" Margin="5 0 5 0" Source="../../Resource/dxf.png"/>
|
||||||
|
|||||||
@ -44,6 +44,8 @@ namespace WeldingCommon.Views
|
|||||||
public ICommand CMD_LaserGuideBeam { get; }
|
public ICommand CMD_LaserGuideBeam { get; }
|
||||||
public ICommand CMD_LaserReadyCheck { get; }
|
public ICommand CMD_LaserReadyCheck { get; }
|
||||||
public ICommand CMD_ImportObjectFromDXF { get; }
|
public ICommand CMD_ImportObjectFromDXF { get; }
|
||||||
|
public ICommand CMD_ToggleScannerCamView { get; }
|
||||||
|
public ICommand CMD_ShowSelectFieldWindow { get; }
|
||||||
|
|
||||||
private readonly IAuroraEnvironment _Aurora;
|
private readonly IAuroraEnvironment _Aurora;
|
||||||
private SystemParameter _SystemParameter;
|
private SystemParameter _SystemParameter;
|
||||||
@ -102,6 +104,12 @@ namespace WeldingCommon.Views
|
|||||||
|
|
||||||
private SequenceDialog _SequenceDialog;
|
private SequenceDialog _SequenceDialog;
|
||||||
|
|
||||||
|
// 레시피 화면에서 토글로 띄우는 스캐너 카메라 + 모션 창 (재사용을 위해 Hide/Show)
|
||||||
|
private WDScannerCamMotion _ScannerCamWindow;
|
||||||
|
|
||||||
|
// Scout Field 선택 팝업 (매뉴얼 화면과 동일한 창을 레시피 화면에서도 사용)
|
||||||
|
private WDSelectScoutField _SelectFieldWindow;
|
||||||
|
|
||||||
private WinSequenceDiagram _WinSequenceDiagramScoutNormal;
|
private WinSequenceDiagram _WinSequenceDiagramScoutNormal;
|
||||||
private WinSequenceDiagram _WinSequenceDiagramScoutFlyingVision;
|
private WinSequenceDiagram _WinSequenceDiagramScoutFlyingVision;
|
||||||
private WinSequenceDiagram _WinSequenceDiagramTeachingRun;
|
private WinSequenceDiagram _WinSequenceDiagramTeachingRun;
|
||||||
@ -139,6 +147,8 @@ namespace WeldingCommon.Views
|
|||||||
CMD_LaserGuideBeam = new DelegateCommand(PerformCmdLaserGuideBeam);
|
CMD_LaserGuideBeam = new DelegateCommand(PerformCmdLaserGuideBeam);
|
||||||
CMD_LaserReadyCheck = new DelegateCommand(PerformCmdLaserReadyCheck);
|
CMD_LaserReadyCheck = new DelegateCommand(PerformCmdLaserReadyCheck);
|
||||||
CMD_ImportObjectFromDXF = new DelegateCommand(PerformCmdImportObjectFromDXF);
|
CMD_ImportObjectFromDXF = new DelegateCommand(PerformCmdImportObjectFromDXF);
|
||||||
|
CMD_ToggleScannerCamView = new DelegateCommand(PerformCmdToggleScannerCamView);
|
||||||
|
CMD_ShowSelectFieldWindow = new DelegateCommand(PerformCmdShowSelectFieldWindow, CanShowSelectFieldWindow);
|
||||||
|
|
||||||
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_SETTING_CHANGED)
|
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_SETTING_CHANGED)
|
||||||
.ObserveOnDispatcher()
|
.ObserveOnDispatcher()
|
||||||
@ -511,6 +521,47 @@ 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 bool CanShowSelectFieldWindow(object o) => _SelectFieldWindow == null;
|
||||||
|
|
||||||
|
private void PerformCmdShowSelectFieldWindow(object o)
|
||||||
|
{
|
||||||
|
// 매뉴얼 화면과 동일한 Scout Field 선택 팝업을 띄움. 저장은 팝업 VM(PerformCmdSave)에서 자체 처리.
|
||||||
|
try {
|
||||||
|
Application.Current.Dispatcher.Invoke(
|
||||||
|
new Action(() => {
|
||||||
|
if (_SelectFieldWindow != null) {
|
||||||
|
_SelectFieldWindow.Close();
|
||||||
|
_SelectFieldWindow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_SelectFieldWindow = new WDSelectScoutField(_Aurora) {
|
||||||
|
Owner = Application.Current.MainWindow
|
||||||
|
};
|
||||||
|
_SelectFieldWindow.Closed += (sender, _) => { _SelectFieldWindow = null; };
|
||||||
|
_SelectFieldWindow.Show();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Log.E($"[PerformCmdShowSelectFieldWindow] 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PerformCmdImportObjectFromDXF(object obj)
|
private void PerformCmdImportObjectFromDXF(object obj)
|
||||||
{
|
{
|
||||||
System.Windows.Forms.FileDialog dlg = new System.Windows.Forms.OpenFileDialog();
|
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>
|
||||||
<Compile Include="Views\Recipes\UCScannerObjectTeachingControlBoxViewModel.cs" />
|
<Compile Include="Views\Recipes\UCScannerObjectTeachingControlBoxViewModel.cs" />
|
||||||
<Compile Include="Views\Recipes\UCScannerObjectTeachingViewModel.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">
|
<Compile Include="Views\Test\UCTestDevice.xaml.cs">
|
||||||
<DependentUpon>UCTestDevice.xaml</DependentUpon>
|
<DependentUpon>UCTestDevice.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
@ -704,6 +707,10 @@
|
|||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</Page>
|
</Page>
|
||||||
|
<Page Include="Views\Recipes\WDScannerCamMotion.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
<Page Include="Views\Test\UCTestDevice.xaml">
|
<Page Include="Views\Test\UCTestDevice.xaml">
|
||||||
<SubType>Designer</SubType>
|
<SubType>Designer</SubType>
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<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