NohDoyeon241104 bd6a7cd21f fix: 레시피 저장 교차오염 트리거 차단 (ISSUE-260904-02)
OnActivatedRecipe 의 무조건 SaveRecipe 를 활성==선택 조건부로 변경하고 불일치 시 스킵 로그. PreSaved/OnSaved 에 활성!=선택 저장 차단과 안내 모달 추가. ISSUE-260826-04 의 트리거 차단이며 근본 해결은 아님. 현장에서 차단 동작 확인 완료.
2026-09-04 14:13:44 +09:00

291 lines
14 KiB
C#

using Aurora;
using Aurora.Api;
using Aurora.Api.Manager;
using Aurora.Framework.Core;
using Aurora.View;
using CoreUnit.Control.UserControl.Canvas;
using CoreUnit.Scanner.UserControl.ScannerCanvas;
using DB.Recipe;
using GUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using VisionCommon;
using WeldingCommon.Alarm;
using WeldingCommon.Devices;
using WeldingCommon.Recipes;
using WeldingCommon.Settings;
using WeldingCommon.Utils;
using WeldingCommon.Utils.Diff;
namespace WeldingCommon.Views
{
public class UCScannerObjectTeachingViewModel : NotifyPropertyChangedBase, IRecipeViewModel
{
private readonly Logger Log = new Logger("UCScannerObjectTeachingViewModel");
private readonly IAuroraEnvironment _Aurora;
private readonly IRecipeManager _RecipeManager;
private readonly IModelManager _ModelManager;
private PLCController _PLC;
private ScannerCanvasUserCtrl _ScannerCanvasUserCtrl;
public bool IsManualStatus { get => _IsManualStatus; set => SetProperty(ref _IsManualStatus, value); }
private bool _IsManualStatus = true;
public UCScannerObjectTeachingControlBoxViewModel ScannerObjectTeachingControlBoxViewModel { get; }
private RecipeWeldingLines _RecipeBeforeChanged;
public UCScannerObjectTeachingViewModel(IAuroraEnvironment aurora)
{
_Aurora = aurora;
_RecipeManager = aurora.RecipeManager;
_ModelManager = aurora.ModelManager;
_PLC = aurora.DeviceManager[CommonDeviceIds.PLCDevice] as PLCController;
ScannerObjectTeachingControlBoxViewModel = new UCScannerObjectTeachingControlBoxViewModel(_Aurora);
// For LASA image update, after scout
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_SETTING_CHANGED)
.ObserveOnDispatcher()
.Subscribe(OnSettingReloaded);
// New, Delete, Copy, Rename
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_RECIPE_ENTRY_CHANGED)
.ObserveOnDispatcher()
.Subscribe(OnRecipeEntryChanged);
_Aurora.EventObservable.AsObservable(AuroraSysEvent.SYS_RECIPE_ACTIVATED)
.ObserveOnDispatcher()
.Subscribe(OnActivatedRecipe);
_Aurora.EventObservable.AsObservable(CommonEventIds.EVENT_EQUIPMENT_STATUS_CHANGE)
.Subscribe(x => OnChangedEquipmentStatus());
RecipeHelper.OnActiveReloaded();
CommonHelper.UpdateAutoRunPage();
}
private object OnChangedEquipmentStatus()
{
IsManualStatus = Status.EquipmentStatus == eEqpStatus.Manual;
//_Aurora.RecipeManager.SaveLock(CommonRecipeIds.ScannerObjectTeaching, !IsManualStatus);
return null;
}
private void OnSettingReloaded(object obj)
{
if (obj is EventMessage msg && msg.Param is string target && msg != null) {
if (target != CommonSettingIds.SystemParameter) {
Log.I($"Target is not {CommonSettingIds.SystemParameter} Update Skip");
return;
}
if (msg.From == "SeqAutoRunLHS_New"
|| msg.From == "SeqAutoRunPrismaticScanner_New"
|| msg.From == "UCMainAutoRunViewModel") {
Log.I($"SystemParameter Update Skip >> From({msg.From})");
return;
}
}
// ScannerCanvasCtrlTeaching Update
CommonHelper.GetMainPage().ReloadSetting();
}
private void OnRecipeEntryChanged(object obj)
{
Log.I("SYS_RECIPE_ENTRY_CHANGED, OnRecipeEntryChanged");
if (obj == null || !(obj is EventMessage msg)) {
return;
}
if (msg.Param is RecipeEntryChangedParam param) {
if (param.AddedId != -1) { //New
var recipe = _RecipeManager.Recipes().FirstOrDefault(x => x.Id == param.AddedId);
if (recipe == null) return;
RecipeHelper.SelectedRecipe = (recipe.Id, recipe.Name);
RecipeHelper.SaveRecipe(recipe.Name, includeModel: false);
} else if (param.RemovedId != -1) { //Remove
} else if (param.CopiedToId != -1 && param.CopiedFromId != -1) { //Copy
} else if (param.RenameNewId != -1 && param.RenameOldId != -1) { //Rename
} else {
string stringFormat = string.Format($"Error : OnRecipeEntryChanged\n" +
$"[Add] [{param.AddedId}]\n" +
$"[Remove] [{param.RemovedId}]\n" +
$"[Copy] [{param.CopiedFromId}] [{param.CopiedToId}]\n" +
$"[Rename] [{param.RenameOldId}] [{param.RenameNewId}]");
MessageUtil.CreateAndShowMessageDialog("Error", stringFormat);
}
}
}
public void OnActivatedRecipe(object obj)
{
Log.D("SYS_RECIPE_ACTIVATED OnActivatedRecipe");
if (obj == null || !(obj is EventMessage msg)) {
return;
}
// @deprecated temporary support
// Recipe를 선택하는데 Z축 동작이 되는 것은 굉장히 위험한 상황이다.
// 한시적으로만 사용한다..
//LHS 라인에서 Recipe Rename시 Mdoel이 Activate 되지 않아서 Rename시가 아니라
//Recipe Active시 Model Active 될 수 있도로 추가
string modelname = _ModelManager.Models().FirstOrDefault();
if (modelname.Trim() == "" || modelname == null) {
string stringFormat = string.Format($"Error : Model, 현재 Recipe에 Model list가 없습니다.");
MessageUtil.CreateAndShowMessageDialog("Error", stringFormat);
return;
}
_ModelManager.ActivateModel(modelname);
Log.I($"Activate Model Name : {modelname}");
var activeRecipe = _RecipeManager.ActivatedRecipe();
// [레시피 덮어쓰기 이슈] 오토/메뉴얼 전환 시 저장 제거
// 활성 전환 직후 메모리는 아직 이전 레시피 내용이다(LASA 는 레시피를 메모리에 1개만 유지).
// 그 상태에서 무조건 저장하면 새 활성 레시피 파일에 이전 내용이 덮어써진다(교차오염).
// ※ 완전 제거가 아니라 신원이 일치할 때만 저장하도록 좁혔다.
// OnRecipeEntryChanged 의 Copy/Rename 분기가 비어 있어(:118-121)
// 이 저장이 새 레시피 폴더의 TeachingEditor 파일 생성을 겸했을 가능성이 있기 때문.
// 스킵 로그는 현장 발생 빈도 확인용이다. 정상 운영(활성=선택)에서는 저장이 그대로 수행된다.
var selectedNameOnActivate = RecipeHelper.SelectedRecipe.name;
if (string.Equals(activeRecipe.Name, selectedNameOnActivate, StringComparison.OrdinalIgnoreCase)) {
RecipeHelper.SaveRecipe(activeRecipe.Name);
} else {
Log.W($"[레시피 덮어쓰기 차단] OnActivatedRecipe 자동저장 스킵. " +
$"활성=[{activeRecipe.Name}] 선택=[{selectedNameOnActivate}]");
}
// Aurora에서 제공하는 이벤트(SYS_RECIPE_ACTIVATED)를 체인으로 사용하면 동작(Aurora가 저장 후 LASA가 저장하는)에 대한 우선순위가 보장이 안되기 때문에
_Aurora.EventPublisher.Publish(new EventMessage("OnActivatedRecipe", CommonEventIds.EVENT_CHANGE_RECIPE_INFO, activeRecipe));
Log.V("CommonEventIds.EVENT_CHANGE_RECIPE_INFO from [OnActivatedRecipe]");
}
public void OnChanged((int id, string name) recipe)
{
RecipeHelper.SelectedRecipe = recipe;
RecipeHelper.ReloadRecipe(recipe.name);
_RecipeBeforeChanged = RecipeHelper.getRecipeInfo(recipe.name);
}
public void Reloaded((int id, string name) recipe)
{
RecipeHelper.SelectedRecipe = recipe;
RecipeHelper.ReloadRecipe(recipe.name);
}
public void OnModelSelectChanged(string modelName)
{
//throw new NotImplementedException();
}
public bool PreSaved(int id, string recipeName)
{
try {
if (ApplicationExt.PermissionLevel > (uint)PermissionLevel.LEVEL3) {
MessageUtil.ShowErrorToast("Engineer 권한으로 로그인 하시기 바랍니다.", Log);
return false;
}
if (!IsManualStatus) {
MessageUtil.ShowErrorToast("Auto 상태에서 레시피 변경 불가", Log);
return false;
}
// [레시피 덮어쓰기 이슈] 활성 ≠ 선택이면 저장 차단
// 저장 대상은 선택 레시피인데 화면 메모리는 활성 레시피 내용이라,
// 그대로 저장하면 엉뚱한 번호 파일에 다른 레시피 내용이 덮어써진다.
// 운영 원칙(활성 레시피 = 선택 레시피)이 지켜지면 이 분기는 발화하지 않는다.
var activeOnSave = _RecipeManager.ActivatedRecipe();
var selectedOnSave = RecipeHelper.SelectedRecipe.name;
if (activeOnSave != null && !string.IsNullOrWhiteSpace(selectedOnSave) &&
!string.Equals(activeOnSave.Name, selectedOnSave, StringComparison.OrdinalIgnoreCase)) {
Log.E($"[레시피 덮어쓰기 차단] PreSaved 저장 차단. " +
$"저장대상=[{recipeName}] 선택=[{selectedOnSave}] 활성=[{activeOnSave.Name}]");
MessageUtil.CreateAndShowMessageDialog("레시피 저장 차단",
$"활성 레시피와 선택된 레시피가 다릅니다.\n\n" +
$" 선택 레시피 : {selectedOnSave}\n" +
$" 활성 레시피 : {activeOnSave.Name}\n\n" +
$"이대로 저장하면 다른 레시피 내용이 덮어써집니다.\n" +
$"레시피 목록에서 저장할 레시피를 다시 선택한 뒤 저장하십시오.");
return false;
}
return true;
} catch (Exception ex) {
Log.E($"PreSaved failed: {ex}");
}
return false;
}
public void OnSaved(int id, string recipeName)
{
if (string.IsNullOrWhiteSpace(RecipeHelper.SelectedRecipe.name)) {
return;
}
// [레시피 덮어쓰기 이슈] 저장 직전 2중 방어.
// PreSaved 가 먼저 막으므로 정상적으로는 도달하지 않는다.
// 도달했다면 Aurora 가 PreSaved=false 를 무시했다는 뜻이라 이미 이상 상황이다.
// ※ 조용히 스킵하면 사용자가 "저장됐다"고 오인하므로 반드시 알린다.
var activeOnSaved = _RecipeManager.ActivatedRecipe();
if (activeOnSaved != null &&
!string.Equals(activeOnSaved.Name, RecipeHelper.SelectedRecipe.name, StringComparison.OrdinalIgnoreCase)) {
Log.E($"[레시피 덮어쓰기 차단] OnSaved 저장 스킵. " +
$"저장대상=[{recipeName}] 선택=[{RecipeHelper.SelectedRecipe.name}] 활성=[{activeOnSaved.Name}]");
MessageUtil.CreateAndShowMessageDialog("레시피 저장 차단",
$"활성 레시피와 선택된 레시피가 달라 저장하지 않았습니다.\n\n" +
$" 선택 레시피 : {RecipeHelper.SelectedRecipe.name}\n" +
$" 활성 레시피 : {activeOnSaved.Name}\n\n" +
$"저장되지 않았습니다. 레시피 목록에서 저장할 레시피를 다시 선택한 뒤 저장하십시오.");
return;
}
RecipeHelper.SaveRecipe(RecipeHelper.SelectedRecipe.name);
var ChangedRecipe = RecipeHelper.getRecipeInfo(RecipeHelper.SelectedRecipe.name);
var diffs = new ObservableCollection<PropertyDiff>();
for (int i = 0; i < 3; i++) {
CompareHelper.Fill(_RecipeBeforeChanged.ReadOnlyScanParams[i], ChangedRecipe.ReadOnlyScanParams[i], diffs,
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.MarkDelay), x => x.MarkDelay),
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.JumpDelay), x => x.JumpDelay),
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.LaserOnDelay), x => x.LaserOnDelay),
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.LaserOffDelay), x => x.LaserOffDelay),
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.LaserFrequency), x => x.LaserFrequency),
new CompareHelper.FieldSpec<ScannerParam>(nameof(ScannerParam.NumberOfCount), x => x.NumberOfCount)
);
if (diffs.Count > 0) {
Log.FuncI($"ScanParameter{i + 1} Recipe Changed");
_Aurora.RecipeManager.RchHandler().AppendChangedItem(RecipeHelper.SelectedRecipe.name + $"-ScanParam{i + 1}", diffs);
foreach (var diff in diffs)
Log.V($"{diff.ToString()}");
}
}
_RecipeBeforeChanged = ChangedRecipe;
var activeRecipe = _RecipeManager.ActivatedRecipe();
if (RecipeHelper.SelectedRecipe.id == activeRecipe.Id) {
CommonHelper.UpdateAutoRunPage();
// Aurora에서 제공하는 이벤트(SYS_RECIPE_ITEM_CHANGED)를 체인으로 사용하면 동작(Aurora가 저장 후 LASA가 저장하는)에 대한 우선순위가 보장이 안되기 때문에
_Aurora.EventPublisher.Publish(new EventMessage("OnSaved", CommonEventIds.EVENT_CHANGE_RECIPE_INFO, activeRecipe));
Log.V($"CommonEventIds.EVENT_CHANGE_RECIPE_INFO from [OnSaved] RecipeName [{RecipeHelper.SelectedRecipe.name}]");
}
Log.I($"Saved Recipe : [{RecipeHelper.SelectedRecipe.name}]");
}
}
}