기동 중 RecipeFieldScan 싱글톤이 비어 있는 상태에서 SaveAndLoadFieldScanFromLasaDb 가 Save 를 Load 보다 먼저 수행해(MainPage.cs:539 -> :540) 대상 FieldHelper.json 이 "[]" 2바이트로 파괴됐다. 현장 3건 확인 — RMS(07-29), U_Frame _pre welding(08-03), Bottom_Frame_Welding(08-03). 3건 모두 Aurora 초기화 직후 4~19초, 사용자 입력이 불가능한 구간이며 형제 TeachingEditor.json 은 옛 mtime 을 유지했다. RecipeFieldScan - SaveFieldHelper 진입부: 완주한 로드의 결과가 아닌 빈 상태면 저장을 건너뛴다. Load(:540)는 그대로 돌아 파일의 실제 데이터로 싱글톤을 채운다. 사용자가 페이지를 지워 정당하게 비운 경우는 loadedOnce 로 구분해 통과시킨다. - LoadFieldHelper: loadedOnce 를 Clear 와 짝지어 리셋하고, 완주해도 결과가 0건이면 로드로 보지 않는다. Clear 이후 예외가 나는 경로에서 플래그만 남아 가드가 무력화되는 것을 막는다. - 맨 catch 를 LastSaveError 기록으로 교체. throw 하지 않는 이유는 ReloadSetting 경로가 Rx OnNext 안이라(UCScannerObjectTeachingViewModel.cs:55-57) 전파 시 구독이 끊겨 티칭 화면 갱신이 영구 정지하기 때문이다. RecipeHelper - SaveRecipe 에 저장 대상과 메모리 신원(Recipe.SelectedRecipeFile) 불일치를 Aurora Logger 로 남기는 관측 코드 추가. 저장은 막지 않는다. 기준을 Dao.SystemRecipe.WorkRecipeFile 로 잡지 않은 것은 그 값이 SystemParameterILSOS.json 에 영속되고 Aurora SystemParameter 와 파일을 공유해 신원으로 쓸 수 없기 때문이다. 교차 오염(내용이 채워진 채 엉뚱한 레시피에 기록되는 현상)은 이 변경으로 막히지 않는다. 관측 로그로 발생 경로를 확정한 뒤 별건으로 조치한다. 검증: LASAGUI/WeldingCommon/ILSOS_PrismaticRD_PTOP 빌드 오류 0, 빌드 산출물 직접 로드한 시뮬레이션 31건 전수 통과.
229 lines
12 KiB
C#
229 lines
12 KiB
C#
using Microsoft.Office.Interop.Excel;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using VisionCommon;
|
|
using static GUI.MainPage;
|
|
using static System.Windows.Forms.VisualStyles.VisualStyleElement;
|
|
|
|
namespace DB
|
|
{
|
|
public class RecipeFieldScan
|
|
{
|
|
private static readonly RecipeFieldScan instance = new RecipeFieldScan();
|
|
|
|
static RecipeFieldScan() { }
|
|
private RecipeFieldScan() { }
|
|
public static RecipeFieldScan Instance { get { return instance; } }
|
|
|
|
private string defaultScoutName = "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_vision\\FieldScanImage\\Default\\DefaultFieldScanImage.Jpeg";
|
|
|
|
#region GUI에서 활용하는 함수 (영역 업데이트, 필드 로드, 필드 저장, 필드 영역 전달 등등)
|
|
public void UpdateScoutField(List<ChildBufRowCol> fieldScan, int selectedIndex)
|
|
{
|
|
if (fieldScanDatas.Count() - 1 < selectedIndex) {
|
|
for (int i = fieldScanDatas.Count() - 1; i < selectedIndex; i++) {
|
|
if (fieldScanDatas.Count() == 0) AddFieldScanData(GetAutoPageName(selectedIndex), defaultScoutName, fieldScan);
|
|
else {
|
|
string str = GetPageImageName(GetAutoPageName(fieldScanDatas.Count - 1), ref defaultScoutName);
|
|
AddFieldScanData(GetAutoPageName(selectedIndex), str, fieldScan);
|
|
}
|
|
}
|
|
} else {
|
|
ChangeFieldScan(GetAutoPageName(selectedIndex), fieldScan);
|
|
}
|
|
}
|
|
public void LoadFieldHelper(string fileName, int pageCount, string defaultFileStr = null, List<ChildBufRowCol> defaultChildBufRowCols=null)
|
|
{
|
|
try {
|
|
XmlDocument doc = new XmlDocument();
|
|
var extenstion = Path.GetExtension(fileName);
|
|
|
|
if (!File.Exists(fileName)) File.WriteAllText(fileName, "");
|
|
|
|
if (extenstion == ".json") {
|
|
var jsonString = File.ReadAllText(fileName);
|
|
} else {
|
|
FileInfo fi = new FileInfo(fileName);
|
|
StreamReader sr = new StreamReader(fileName);
|
|
doc.Load(sr);
|
|
sr.Dispose();
|
|
}
|
|
|
|
XmlElement root = doc.DocumentElement;
|
|
|
|
|
|
// JSON 파일 읽기, JSON을 List로 변환
|
|
string json = File.ReadAllText(fileName);
|
|
List<FieldScanData> jsonData = new List<FieldScanData>();
|
|
|
|
if (json != null && json != "") jsonData = System.Text.Json.JsonSerializer.Deserialize<List<FieldScanData>>(json);
|
|
else ;
|
|
|
|
// [추가 2026-08-04] Clear 와 짝을 맞춘다. 이 아래에서 예외가 나면(:77 f.PageName,
|
|
// :84 js.PageName 의 NRE 등) 리스트는 빈 채 남으므로 플래그도 false 여야 한다.
|
|
loadedOnce = false;
|
|
fieldScanDatas.Clear();
|
|
|
|
// default 값 써주기
|
|
FieldScanData defaultFieldScan = new FieldScanData("none", defaultFileStr, defaultChildBufRowCols);
|
|
|
|
// page 개수보다 입력된 개수가 작을 경우(비정상)와 정상인 경우에 따라 다르게 저장.
|
|
if (jsonData.Count != pageCount) {
|
|
for (int i = 0; i < pageCount; i++) {
|
|
// PageName이 동일하다면 그대로 써줄 것. 아니라면, 초기 값을 써줄 것.
|
|
var modifyFieldScan = jsonData.FirstOrDefault(f => f.PageName.Equals(GetAutoPageName(i), StringComparison.OrdinalIgnoreCase));
|
|
if (modifyFieldScan != null && modifyFieldScan.FieldScans != null && modifyFieldScan.FileName != null) AddFieldScanData(modifyFieldScan.PageName, modifyFieldScan.FileName, modifyFieldScan.FieldScans);
|
|
else AddFieldScanData(GetAutoPageName(i), defaultFieldScan.FileName, defaultFieldScan.FieldScans);
|
|
}
|
|
} else {
|
|
foreach (FieldScanData js in jsonData) {
|
|
if (js != null && js.FieldScans != null && js.FileName != null) AddFieldScanData(js.PageName, js.FileName, js.FieldScans);
|
|
else AddFieldScanData(js.PageName, defaultFieldScan.FileName, defaultFieldScan.FieldScans);
|
|
}
|
|
}
|
|
|
|
// [추가 2026-08-04] 완주했더라도 결과가 0건이면(pageCount==0 등) 로드된 것으로 보지 않는다.
|
|
// ReloadSetting(MainPage.cs:448/452)/UpdateScoutField(:502/:505)가 Save 를 연속 2회 하므로,
|
|
// 여기서 true 로 굳히면 두 번째 Save 가 가드를 통과해 "[]" 를 기록한다.
|
|
loadedOnce = fieldScanDatas.Count > 0;
|
|
} catch (Exception ex) {
|
|
throw ex;
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Scout 후, Field Size 변경 후, Recipe 저장 시 FieldScan 저장되어야 함.
|
|
/// </summary>
|
|
/// <param name="fileName"></param>
|
|
/// <param name="defaultFileStr"></param>
|
|
public void SaveFieldHelper(string fileName, string defaultFileStr = null)
|
|
{
|
|
// [추가 2026-08-04] 완주한 로드의 결과가 아닌 빈 상태를 파일에 확정하지 않는다.
|
|
// fieldScanDatas 는 프로세스 시작 시 빈 리스트인데
|
|
// SaveAndLoadFieldScanFromLasaDb 는 Save 를 먼저 하므로(MainPage.cs:539 -> :540)
|
|
// 기동 중 첫 호출이 대상 파일을 "[]" 로 밀어버린다. 현장 3건 발생 확인.
|
|
// Save 만 건너뛰고 :540 Load 는 그대로 돌아 파일의 실제 데이터로 싱글톤을 채운다.
|
|
// loadedOnce 로 게이트하는 이유: 사용자가 페이지를 지워 정당하게 비운 경우
|
|
// (DeleteFieldScanData / 호출부 MainPage.cs:904-905)는 저장돼야 하기 때문이다.
|
|
if (!loadedOnce && (fieldScanDatas == null || fieldScanDatas.Count == 0)) {
|
|
SkippedEmptySaveCount++;
|
|
SkippedFiles.Add(fileName ?? "(null)");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
using (MemoryStream stream = new MemoryStream()) {
|
|
XmlTextWriter wr = new XmlTextWriter(stream, Encoding.UTF8);
|
|
wr.Formatting = System.Xml.Formatting.Indented;
|
|
wr.Flush();
|
|
|
|
foreach(FieldScanData fieldScan in fieldScanDatas) {
|
|
fieldScan.FieldScans
|
|
.OrderBy(row => row.RowIdx)
|
|
.ThenBy(col => col.ColIdx)
|
|
.Select(param => param.Clone() as ChildBufRowCol)
|
|
.ToList();
|
|
}
|
|
|
|
var json = System.Text.Json.JsonSerializer.Serialize(fieldScanDatas, new JsonSerializerOptions { WriteIndented = true });
|
|
File.WriteAllText(fileName, json);
|
|
LastSaveError = null; // [추가 2026-08-04] 성공했으면 이전 오류를 남기지 않는다
|
|
}
|
|
} catch (Exception ex) {
|
|
// [변경 2026-08-04] 쓰기 실패가 로그 없이 사라져 데이터 소실을 관측할 수 없었다.
|
|
// 단 throw 하지 않는다 - ReloadSetting(MainPage.cs:448/452) 경로가
|
|
// Rx OnNext 안이라(UCScannerObjectTeachingViewModel.cs:55-57 Subscribe)
|
|
// 전파되면 구독이 끊기고 티칭 화면 갱신이 영구 정지한다.
|
|
// MainPage.cs:1433 콤보박스 핸들러에는 try/catch 가 아예 없다.
|
|
LastSaveError = $"{fileName} :: {ex.Message}";
|
|
}
|
|
}
|
|
public List<ChildBufRowCol> GetPageFieldScan(string pageName)
|
|
{
|
|
var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase));
|
|
if (modifyFieldScan != null) {
|
|
return modifyFieldScan.FieldScans
|
|
.OrderBy(row => row.RowIdx)
|
|
.ThenBy(col => col.ColIdx)
|
|
.Select(param => param.Clone() as ChildBufRowCol)
|
|
.ToList();
|
|
} else return null;
|
|
}
|
|
public string GetAutoPageName(int pageNo) { return string.Format("selectedIndex{0}", pageNo); }
|
|
#endregion
|
|
|
|
#region Dictionary 관리를 위한 함수
|
|
void AddFieldScanData(string pageName, string fileName, List<ChildBufRowCol> childBuf)
|
|
{
|
|
fieldScanDatas.Add(new FieldScanData(pageName,fileName,childBuf));
|
|
for (int i = 0; i < fieldScanDatas.Count(); i++) {
|
|
fieldScanDatas[i].PageName = GetAutoPageName(i);
|
|
}
|
|
}
|
|
public void DeleteFieldScanData(string pageName)
|
|
{
|
|
fieldScanDatas = fieldScanDatas.Where(fieldScan => fieldScan.PageName != pageName).ToList();
|
|
for (int i = 0; i < fieldScanDatas.Count(); i++) {
|
|
fieldScanDatas[i].PageName = GetAutoPageName(i);
|
|
}
|
|
}
|
|
bool ChangeFieldScan(string pageName, List<ChildBufRowCol> childBuf)
|
|
{
|
|
var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase));
|
|
if (modifyFieldScan != null) {
|
|
modifyFieldScan.FieldScans = childBuf;
|
|
return true;
|
|
} else return false;
|
|
}
|
|
public bool ChangeFieldString(string pageName, string fileName)
|
|
{
|
|
var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase));
|
|
if (modifyFieldScan != null) {
|
|
modifyFieldScan.FileName = fileName;
|
|
return true;
|
|
} else return false;
|
|
}
|
|
public string GetPageImageName(string pageName, ref string defaultStr)
|
|
{
|
|
var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase));
|
|
if (modifyFieldScan != null) {
|
|
return modifyFieldScan.FileName;
|
|
} else return defaultStr;
|
|
}
|
|
#endregion
|
|
|
|
#region 데이터를 위한 Class
|
|
private List<FieldScanData> fieldScanDatas = new List<FieldScanData>();
|
|
|
|
// [추가 2026-08-04] 빈 상태 저장 차단 및 관측용.
|
|
// 이 프로젝트(LASAGUI)에는 Aurora 참조가 없어 여기서 로그를 남길 수 없으므로
|
|
// 상태만 노출하고, WeldingCommon 쪽(RecipeHelper)에서 Aurora Logger 로 보고한다.
|
|
// loadedOnce 의 의미: "지금 fieldScanDatas 가 완주한 로드의 결과인가".
|
|
// 한 번이라도 성공했는가(write-once)가 아니다 - Clear 이후 예외가 나면 다시 false 가 된다.
|
|
private bool loadedOnce = false;
|
|
public static int SkippedEmptySaveCount = 0;
|
|
public static readonly HashSet<string> SkippedFiles =
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
public static string LastSaveError = null;
|
|
public class FieldScanData
|
|
{
|
|
public string PageName { get; set; }
|
|
public string FileName { get; set; }
|
|
public List<ChildBufRowCol> FieldScans { get; set; }
|
|
|
|
public FieldScanData(string pageName, string fileName, List<ChildBufRowCol> fieldScans)
|
|
{
|
|
this.PageName = pageName;
|
|
this.FileName = fileName;
|
|
this.FieldScans = fieldScans;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
}
|