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(); for (int i = 0; i < 3; i++) { CompareHelper.Fill(_RecipeBeforeChanged.ReadOnlyScanParams[i], ChangedRecipe.ReadOnlyScanParams[i], diffs, new CompareHelper.FieldSpec(nameof(ScannerParam.MarkDelay), x => x.MarkDelay), new CompareHelper.FieldSpec(nameof(ScannerParam.JumpDelay), x => x.JumpDelay), new CompareHelper.FieldSpec(nameof(ScannerParam.LaserOnDelay), x => x.LaserOnDelay), new CompareHelper.FieldSpec(nameof(ScannerParam.LaserOffDelay), x => x.LaserOffDelay), new CompareHelper.FieldSpec(nameof(ScannerParam.LaserFrequency), x => x.LaserFrequency), new CompareHelper.FieldSpec(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}]"); } } }