chore: docs/분석 git 추적 해제 및 gitignore 추가, 산출물 양식 폴더 생성

- /docs/분석/ 을 .gitignore에 추가 (로컬 전용 분석 문서, 커밋 제외)
- 기존 추적되던 docs/분석 22개 파일 git rm --cached (로컬 파일은 유지)
- Doc/산출물문서/양식/ 폴더 생성 (추후 필수 제출 2종 드롭 위치, README 포함)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
노도연 2026-06-20 14:41:04 +09:00
parent 1d60d474b8
commit d537792c48
24 changed files with 33 additions and 3421 deletions

3
.gitignore vendored
View File

@ -41,3 +41,6 @@ bin/
# 로컬 전용 Aurora 에이전트 레퍼런스 (커밋 제외) # 로컬 전용 Aurora 에이전트 레퍼런스 (커밋 제외)
/docs/Aurora에이전트레퍼런스모음/ /docs/Aurora에이전트레퍼런스모음/
# 로컬 전용 분석 문서 (커밋 제외)
/docs/분석/

View File

@ -0,0 +1,30 @@
# 산출물문서 — 양식(제출 세트) 폴더
이 폴더는 **추후 추가될 필수 제출 산출물**을 받아두는 드롭 위치입니다.
파일을 여기에 넣어주시면 동일한 방식으로 분석 → PTOP 프로젝트 내용으로 채움 진행합니다.
## 제출 세트 현황 (총 4종 예정)
| # | 산출물 | 위치 | 상태 |
|---|--------|------|------|
| 23 | SW 점검 결과 (일일 IO Check 진척 공유) `.xlsx` | `Doc/산출물문서/` | 입수 ✅ (PTOP 채움 필요) |
| 27 | HMI Manual `.pptx` | `Doc/산출물문서/` | 입수 ✅ (PTOP 채움 필요) |
| (TBD) | 필수 제출 ① | `Doc/산출물문서/양식/` | **대기 — 여기 넣어주세요** |
| (TBD) | 필수 제출 ② | `Doc/산출물문서/양식/` | **대기 — 여기 넣어주세요** |
## 넣을 때 참고
- 파일명에 설비/날짜 식별자가 있으면 그대로 두셔도 됩니다(예: `*_UBISAM_yymmdd_PTOP.*`).
- 넣은 뒤 알려주시면: ① 구조 분석 → ② 타설비 템플릿 잔재 식별 → ③ PTOP 내용(코드 출처 명시) 채움 초안 작성 순서로 진행합니다.
## 관련 분석 문서
- `docs/분석/2026/06/20/141528_산출물문서_분석_및_PTOP_채움안.md`
— #23(Excel)·#27(PPT)의 시트/슬라이드별 PTOP 채움 초안.
## PTOP 프로젝트 핵심 사실 (채움 시 공통 참조)
- 설비: 각형 배터리 레이저 용접기(ILSOS OC5 PtoP Welder), Aurora 2.0 / .NET 4.8.1 / WPF
- 주요 디바이스: PLC(Mitsubishi Melsec, MX Component), Laser Scanner(RTC), Laser(IO Gate), Ajin Digital IO
- 메인 시퀀스: `SeqAutoRunPTOP` (Recipe/Page 핸드셰이크 → 용접리스트 업로드 → 용접 발진 → 완료비트)
- PLC 인터페이스 정의: `Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs`

View File

@ -1,397 +0,0 @@
# ILSOS_PrismaticRD_PTOP Recipe 동적 활성화 분석
**분석 일시:** 2026-06-16 13:59:37
**대상:** SeqAutoRunPTOP.cs + Aurora RecipeManager 메커니즘
**목표:** PLC에서 RecipeNum + PageNum을 받아 동적으로 Recipe를 변경하고 공정별 페이지 선택하는 설계
---
## 1. 현재 상황 분석
### 1.1 현재 구조
```
SeqAutoRunPTOP.cs (Line 215-216):
├─ activatedRecipeId = Application.Current.AuroraEnv().RecipeManager.ActivatedRecipe().Id
├─ recipeName = RecipeHelper.SearchRecipeItem(activatedRecipeId)
└─ 활성화된 Recipe 1개에 대해서만 작동
└─ PLC에서 PageNum(1,2)만 받음 → 해당 Recipe의 페이지 선택
```
### 1.2 현재 제약사항
- **단일 Recipe 지원**: 미리 활성화된 Recipe 1개만 사용
- **동적 변경 불가**: Recipe 활성화 코드 없음 (UI에서만 가능)
- **공정 분리 불가**: 3개 공정(CC, CAP, U-Frame)을 구분할 수 없음
---
## 2. Aurora RecipeManager 메커니즘 분석
### 2.1 Recipe 활성화 방법
**발견 위치:** `RMSHandlerForScanner.cs` Line 173
```csharp
// Recipe 활성화 핵심 메서드
_aurora.RecipeManager.ActivateRecipe(recipe.Id, "RMS Request");
```
**메서드 시그니처:**
```csharp
void ActivateRecipe(int recipeId, string reason)
```
**매개변수:**
- `recipeId` (int): Recipe의 고유 ID
- `reason` (string): 활성화 사유 (로그/감사용) - 예: "RMS Request", "Manual Change", "Auto Selection"
### 2.2 Recipe 정보 조회
**Recipe ID로 조회:**
```csharp
// RMSHandlerForScanner.cs Line 125
var recipe = _recipeEntry.Recipes().FirstOrDefault(x => x.Id == recipeId);
```
**Recipe 이름으로 조회:**
```csharp
// RMSHandlerForScanner.cs Line 132
var recipeInfo = RecipeHelper.getRecipeInfo(recipe.Name);
```
### 2.3 Recipe 로드/저장
**Recipe 로드 (활성화 후):**
```csharp
// RMSHandlerForScanner.cs Line 136
RecipeHelper.ReloadRecipe(recipe.Name);
```
**Recipe 저장:**
```csharp
// RMSHandlerForScanner.cs Line 146
RecipeHelper.SaveRecipe(recipe.Name);
```
### 2.4 이벤트 트리거
**Recipe 변경 이벤트:**
```csharp
// EquipDefCommon.cs Line 145
public const string EVENT_CHANGE_RECIPE_INFO = "EventChangeRecipeInfo";
// RMSHandlerForScanner.cs Line 56-59 (구독 예시)
aurora.EventObservable.AsObservable(CommonEventIds.EVENT_CHANGE_RECIPE_INFO)
.ObserveOnDispatcher()
.Select(param => (RecipeInfo)param.Param)
.Subscribe(OnUpdateCurrentActivatedRecipe);
```
**활성화 완료 흐름:**
```
RecipeManager.ActivateRecipe(id, reason)
↓ (EVENT_CHANGE_RECIPE_INFO 발행)
↓
RMSHandlerForScanner.OnUpdateCurrentActivatedRecipe(RecipeInfo info)
├─ RecipeHelper.ReloadRecipe(info.Name)
└─ UploadActivatedRecipeParameter()
```
---
## 3. 변경 요구사항 분석
### 3.1 요구사항
| # | 요구사항 | 현재 상태 | 변경 필요 |
|---|---------|---------|----------|
| 1 | 3개 공정 지원 (CC, CAP, U-Frame) | 단일 Recipe만 지원 | ✓ 다중 Recipe 매핑 |
| 2 | 각 공정별 Recipe 활성화 | UI에서만 가능 | ✓ PLC 신호로 동적 활성화 |
| 3 | RecipeNum + PageNum 수신 | PageNum만 수신 | ✓ RecipeNum WORD 추가 |
| 4 | RecipeNum → RecipeId 매핑 | 없음 | ✓ 설정 테이블 추가 |
| 5 | UI에서 공정별 RecipeNum 관리 | 없음 | ✓ 설정 UI 추가 (후순위) |
### 3.2 예상 동작
**변경 전:**
```
PLC → ReadWord(PLC_PAGE_NO) → PageNum(1,2)
↓
SeqAutoRunPTOP → 활성된 Recipe의 Page 선택
↓
_SelectedObjectList 구성
```
**변경 후:**
```
PLC → ReadWord(PLC_RECIPE_NO) + ReadWord(PLC_PAGE_NO)
↓
SeqAutoRunPTOP → RecipeNum으로 Recipe 동적 활성화
↓
RecipeManager.ActivateRecipe(mappedRecipeId, "PLC Request")
↓
EVENT_CHANGE_RECIPE_INFO 발생
↓
해당 Recipe 로드 + 설정 적용
↓
Page 선택 → _SelectedObjectList 구성
```
---
## 4. 구현 계획
### Phase 1: 설정 테이블 정의
**위치:** SeqAutoRunPTOP.cs 또는 별도 설정 클래스
```csharp
// 공정별 RecipeNum ↔ RecipeId 매핑
public class ProcessRecipeMapping
{
public enum eProcessType
{
CC = 1, // 공정1
CAP = 2, // 공정2
UFrame = 3 // 공정3
}
// 매핑 테이블 (사용자 설정 후 저장)
private Dictionary<eProcessType, int> _recipeIdMap = new Dictionary<eProcessType, int>()
{
{ eProcessType.CC, 101 }, // CC 공정 → Recipe ID 101
{ eProcessType.CAP, 102 }, // CAP 공정 → Recipe ID 102
{ eProcessType.UFrame, 103 } // U-Frame 공정 → Recipe ID 103
};
public int GetRecipeId(eProcessType process) => _recipeIdMap[process];
public int GetRecipeId(int recipeNum) => _recipeIdMap[(eProcessType)recipeNum];
}
```
### Phase 2: PLC 신호 정의
**SeqAutoRunPTOP.cs에 추가:**
```csharp
// PLC에서 수신할 WORD 정의
private int _plcRecipeNum = 0; // PLC_RECIPE_NO (Word)
private int _plcPageNum = 0; // PLC_PAGE_NO (Word)
// CheckPageNo 시퀀스 수정 위치
// Line 354: plcPageNoList.Add(_PLC.ReadWord(eWordInput.PLC_PAGE_NO).data);
//
// 변경 후:
// ┌─────────────────────────────────────────┐
// │ Step 1: RecipeNum 읽기 │
// │ _plcRecipeNum = _PLC.ReadWord(...) │
// │ │
// │ Step 2: RecipeId 매핑 │
// │ int recipeId = mappingTable.GetRecipeId(_plcRecipeNum) │
// │ │
// │ Step 3: Recipe 활성화 │
// │ _AuroraEnv.RecipeManager.ActivateRecipe │
// │ (recipeId, "PLC Request") │
// │ │
// │ Step 4: Recipe 로드 완료 대기 │
// │ (이벤트 핸들러에서 처리) │
// │ │
// │ Step 5: PageNum 읽기 │
// │ _plcPageNum = _PLC.ReadWord(...) │
// └─────────────────────────────────────────┘
```
### Phase 3: 코드 구조 변경
**SeqAutoRunPTOP.CheckPageNo() 수정 전략:**
```csharp
// 현재 (Line 282-333):
private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param)
{
// PLC_PAGE_CHANGE_REQ 대기 → PageNum 읽기 → 완료
}
// 변경 후:
private SequenceActionResult CheckPageNo(SequenceContext context, string id, SequenceActionResult param)
{
// Step 1: PLC_PAGE_CHANGE_REQ 대기 (기존 로직)
// Step 2: RecipeNum 읽기 추가
// Step 3: Recipe 활성화 (동기/비동기 결정)
// Step 4: Recipe 로드 완료 확인
// Step 5: PageNum 읽기 (기존 로직)
// Step 6: 완료
}
```
**Recipe 활성화 이벤트 핸들러:**
```csharp
// SeqAutoRunPTOP 클래스 내부
private bool _recipeChangeCompleted = false;
// Aurora 이벤트 구독 (Sequence_Enter에서)
_AuroraEnv.EventObservable
.AsObservable(CommonEventIds.EVENT_CHANGE_RECIPE_INFO)
.ObserveOnDispatcher()
.Subscribe(msg =>
{
Log.I("[CheckPageNo] Recipe Change Event 감지");
_recipeChangeCompleted = true;
});
// CheckPageNo에서 완료 대기
private SequenceActionResult WaitForRecipeChange(SequenceContext context, int timeoutSec = 5)
{
_recipeChangeCompleted = false;
var startTime = DateTime.Now;
while (!context.Stopped && !_recipeChangeCompleted)
{
if (DateTime.Now.Subtract(startTime).TotalSeconds > timeoutSec)
{
Log.E("[CheckPageNo] Recipe Change Timeout");
return null;
}
Thread.Sleep(10);
}
Log.I("[CheckPageNo] Recipe Change 완료");
return SequenceAction.Ok();
}
```
### Phase 4: RecipeHelper를 통한 로드 확인
```csharp
// RMSHandlerForScanner.cs의 OnUpdateCurrentActivatedRecipe 패턴 활용
private void OnRecipeActivated(RecipeInfo info)
{
Log.I($"[SeqAutoRunPTOP] Recipe 활성화됨: {info.Name}");
// Recipe 파라미터 로드
RecipeHelper.ReloadRecipe(info.Name);
// 필요시 로드 검증
var loadedRecipe = RecipeHelper.getRecipeInfo(info.Name);
if (loadedRecipe == null)
{
Log.E("[SeqAutoRunPTOP] Recipe 로드 실패");
AlarmManager.SetAlarm(AlarmCode.RECIPE_LOAD_FAILED);
return;
}
Log.I("[SeqAutoRunPTOP] Recipe 로드 완료");
_recipeChangeCompleted = true;
}
```
---
## 5. 구현 순서 (권장)
### Step 1: 설정 클래스 생성
- `ProcessRecipeMapping` 클래스 작성
- RecipeNum ↔ RecipeId 매핑 설정
### Step 2: SeqAutoRunPTOP.cs 수정
1. 이벤트 핸들러 추가 (Recipe 활성화 완료 감지)
2. CheckPageNo에 RecipeNum 읽기 로직 추가
3. RecipeManager.ActivateRecipe() 호출
4. Recipe 로드 완료 대기
### Step 3: PLC 통신 검증
- ReadWord(RecipeNum) 정상 작동 확인
- Recipe 활성화 타이밍 검증
- PageNum 읽기가 Recipe 로드 후 실행되는지 확인
### Step 4: 에러 처리 추가
- Recipe ID 미존재 시 처리
- Recipe 로드 타임아웃 처리
- Fallback 로직 (기본 Recipe 활용)
### Step 5: UI 설정 추가 (후순위)
- 공정별 RecipeNum 설정 화면
- 매핑 테이블 UI 편집
---
## 6. 예상 코드 위치
| 파일 | 라인 | 수정 내용 |
|------|------|---------|
| SeqAutoRunPTOP.cs | 50-100 | 클래스 멤버 추가 (매핑, 이벤트) |
| SeqAutoRunPTOP.cs | 202-240 | Sequence_Enter에서 이벤트 구독 |
| SeqAutoRunPTOP.cs | 282-333 | CheckPageNo에 RecipeNum 읽기 추가 |
| 별도 파일 | - | ProcessRecipeMapping 클래스 |
---
## 7. 핵심 검증 포인트
1. **RecipeManager.ActivateRecipe() 호출 후 Event 발생 타이밍**
- 동기인가 비동기인가?
- 즉시 발생하는가 아니면 지연되는가?
2. **RecipeHelper.ReloadRecipe()의 필요성**
- EVENT_CHANGE_RECIPE_INFO에서 자동으로 로드되는가?
- 수동으로 ReloadRecipe를 호출해야 하는가?
3. **_SelectedObjectList 업데이트 시점**
- Recipe 활성화 후 새 Recipe의 Canvas가 로드되는가?
- 별도로 처리해야 하는가?
4. **타이밍 문제**
- RecipeNum 읽기 → 활성화 → 로드 완료 → PageNum 읽기
- 각 단계의 예상 소요시간?
---
## 8. 참고 코드
### RecipeManager.ActivateRecipe 사용 사례
**파일:** `RMSHandlerForScanner.cs` Line 173
```csharp
var preActivatedRecipe = _aurora.RecipeManager.ActivatedRecipe();
_aurora.RecipeManager.ActivateRecipe(recipe.Id, "RMS Request");
MessageUtil.ShowToast($"RMS Change Recipe ... ( {preActivatedRecipe.Id} -> {recipe.Id} )", Log);
```
### EVENT_CHANGE_RECIPE_INFO 구독 사례
**파일:** `RMSHandlerForScanner.cs` Line 56-66
```csharp
aurora.EventObservable.AsObservable(CommonEventIds.EVENT_CHANGE_RECIPE_INFO)
.ObserveOnDispatcher()
.Select(param => (RecipeInfo)param.Param)
.Subscribe(OnUpdateCurrentActivatedRecipe);
private void OnUpdateCurrentActivatedRecipe(RecipeInfo info)
{
RecipeHelper.ReloadRecipe(info.Name);
UploadActivatedRecipeParameter();
}
```
---
## 9. 다음 단계
1. **코드 분석 후 결정 필요:**
- RecipeManager.ActivateRecipe()의 정확한 동작 방식 (동기/비동기)
- EVENT_CHANGE_RECIPE_INFO 발생 타이밍
- RecipeHelper.ReloadRecipe()의 필요성
2. **설계 결정:**
- RecipeNum을 WORD로 받을 것인가? (또는 별도 IO?)
- 매핑 테이블을 하드코딩할 것인가? (또는 설정 파일?)
- Fallback 시나리오 정의
3. **구현 시작:**
- 위 확인 후 Phase 1부터 순차 진행
---
**작성자:** Claude Code Analysis
**검토 필요:** 실제 Aurora RecipeManager 메서드 시그니처 확인 필요

View File

@ -1,430 +0,0 @@
# Recipe 동적 활성화 — 깊이 있는 분석 (상세 검증)
**분석 일시:** 2026-06-16 13:59:37
**목표:** RecipeManager.ActivateRecipe()의 동작 방식과 이벤트 발생 타이밍, ReloadRecipe()를 통한 변경 검증 방법 명확화
---
## 1. RecipeManager.ActivateRecipe() 상세 분석
### 1.1 메서드 시그니처와 반환값
**위치:** `UCRecipeSelectViewModel.cs` Line 112
```csharp
int err = _RecipeManager.ActivateRecipe(SelectedRecipe.Id, "USER_via_CustomUI");
if (err != E1.OK) {
LOG.FuncE("Activate Fail");
}
ActivatedRecipe = SelectedRecipe;
```
**결론: 동기(Synchronous) 작동**
| 특성 | 값 | 근거 |
|------|-----|------|
| **작동 방식** | **동기** | 반환값 확인 후 즉시 ActivatedRecipe 업데이트 |
| **반환 타입** | int (에러 코드) | E1.OK = 성공, 그 외 = 실패 |
| **성공 조건** | err == E1.OK | 에러 체크 후 진행 |
| **동작 보장** | 반환 시점에 완료됨 | 다음 라인 실행 전 완전히 활성화됨 |
### 1.2 동기 작동의 의미
```
[ActivateRecipe 호출]
↓
[RecipeManager 내부에서:
- 현재 Recipe 종료
- 새 Recipe 활성화
- 내부 상태 업데이트]
↓
[int err 반환 (이 시점에 모든 작업 완료)]
↓
[호출처에서 err == E1.OK 확인]
↓
[다음 작업 진행 (ActivatedRecipe 업데이트, 등)]
```
**당신의 직관이 맞습니다:** "작업이 끝난 다음 시도해야할 것으로 보입니다" → 100% 정확
---
## 2. EVENT_CHANGE_RECIPE_INFO 발생 타이밍 분석
### 2.1 두 가지 이벤트 경로
**Aurora 시스템 이벤트:**
```csharp
// UCRecipeSelectViewModel.cs Line 74
_EventObservable.AsObservable(AuroraSysEvent.SYS_RECIPE_ACTIVATED)
.Select(x => (RecipeInfo)x.Param)
.Subscribe(x => SelectRecipe(x));
```
- **언제**: ActivateRecipe() 성공 후
- **누가**: Aurora RecipeManager 내부에서 자동 발행
- **신뢰성**: Aurora 자체에서 보장
**커스텀 이벤트:**
```csharp
// UCScannerObjectTeachingViewModel.cs Line 159
_Aurora.EventPublisher.Publish(new EventMessage(
"OnActivatedRecipe",
CommonEventIds.EVENT_CHANGE_RECIPE_INFO,
activeRecipe
));
```
- **언제**: RecipeHelper.SaveRecipe() 완료 후
- **누가**: 사용자가 수동으로 발행
- **목적**: Aurora 이벤트보다 높은 우선순위 보장
- **주석**: "Aurora가 저장 후 LASA가 저장하는 동작의 우선순위가 보장 안 되기 때문에"
### 2.2 이벤트 발생 순서 (실제 흐름)
```
[PLC에서 RecipeNum 신호]
↓
[SeqAutoRunPTOP에서 ReadWord(RecipeNum)]
↓
[RecipeManager.ActivateRecipe(recipeId, reason)]
↓
[① ActivateRecipe() 내부:
- Recipe 활성화 완료
- SYS_RECIPE_ACTIVATED 자동 발행]
↓
[② int err 반환 (err == E1.OK)]
↓
[③ 호출처에서 EVENT_CHANGE_RECIPE_INFO를 수동으로 발행]
(또는 이미 구독 중인 SYS_RECIPE_ACTIVATED 처리)
↓
[④ 이벤트 핸들러 실행:
- RecipeHelper.ReloadRecipe()
- 설정 적용]
```
### 2.3 결론: 50ms 지연이 필요한 이유
**당신의 분석 정확:**
- ActivateRecipe()는 동기이지만, **내부 이벤트 발행 → 이벤트 핸들러 처리**는 비동기일 수 있음
- 특히 UI 스레드 관련 작업은 **Dispatcher 큐를 타고 실행**됨
- **50ms 지연** = 이벤트 핸들러 완료 대기
```csharp
// 권장 패턴
int err = _AuroraEnv.RecipeManager.ActivateRecipe(recipeId, "PLC Request");
if (err != E1.OK) {
Log.E("Recipe Activation Failed");
return null;
}
Log.I($"Recipe Activated: {recipeId}");
Thread.Sleep(50); // ← 이벤트 핸들러 완료 대기
// 이 시점에 ReloadRecipe()가 완료됨
var loadedRecipe = RecipeHelper.getRecipeInfo(recipeName);
```
---
## 3. ReloadRecipe()를 통한 변경사항 검증 (더블 체크)
### 3.1 ReloadRecipe()의 역할
**위치:** `RecipeHelper.cs` Line 64-69
```csharp
public static void ReloadRecipe(string recipeName)
{
var camConfig = VisionEnv.AuroraEnv.DeviceManager.GetDeviceConfig(
eCamId.CAM1.ToString()) as CamConfig;
CommonHelper.GetMainPage().UpdateDefaultField(camConfig.ChildImage.ChildBufRowCols);
CommonHelper.GetMainPage().ReloadRecipe(GetTeachingEditorFullPath(recipeName));
}
```
**역할:**
- 파일에서 Recipe 데이터를 **메모리로 로드**
- UI 캔버스에 그릴 수 있도록 준비
- 스캐너 파라미터 등을 메모리 구조로 변환
### 3.2 getRecipeInfo()를 통한 검증
**위치:** `RecipeHelper.cs` Line 71
```csharp
public static RecipeWeldingLines getRecipeInfo(string recipeName) =>
CommonHelper.GetMainPage().getRecipeInfo(GetTeachingEditorFullPath(recipeName));
```
**반환 타입:** `RecipeWeldingLines`
- 현재 메모리에 로드된 Recipe 정보
- 용접 라인, 파라미터 등 포함
### 3.3 변경사항 검증 패턴 (RMSHandlerForScanner 참고)
**위치:** `RMSHandlerForScanner.cs` Line 132-149
```csharp
// Step 1: 이전 Recipe 정보 저장
var RecipeBeforeChanged = RecipeHelper.getRecipeInfo(recipe.Name);
// Step 2: Recipe 활성화
Application.Current.Dispatcher.Invoke(() => {
RecipeHelper.ReloadRecipe(recipe.Name);
});
// Step 3: 파라미터 적용 (RMS에서 받은 데이터 적용)
for (int i = 0; i < 3; i++) {
if (!ApplyReportToParam(ScannerCanvasUserCtrl.ScannerParams[i], i))
return;
}
// Step 4: 변경사항 저장 및 재로드
Application.Current.Dispatcher.Invoke(() => {
RecipeHelper.SaveRecipe(recipe.Name);
});
// Step 5: 변경된 정보 확인
var ChangedRecipe = RecipeHelper.getRecipeInfo(recipe.Name);
// Step 6: 변경사항 비교 (CompareHelper 사용)
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>("Draw Speed", x => x.MarkSpeed),
// ... 기타 필드 비교
);
}
// Step 7: 변경사항 로깅
if (diffs.Count > 0) {
Log.FuncI($"ScanParameter{i + 1} Recipe Changed");
foreach (var diff in diffs)
Log.V($"{diff.ToString()}");
}
// Step 8: Activated Recipe 확인
var preActivatedRecipe = _aurora.RecipeManager.ActivatedRecipe();
_aurora.RecipeManager.ActivateRecipe(recipe.Id, "RMS Request");
```
### 3.4 SeqAutoRunPTOP을 위한 더블 체크 패턴
```csharp
// ===== 제안 패턴 =====
// 이전 Recipe 정보 저장
var previousRecipeInfo = RecipeHelper.getRecipeInfo(
_AuroraEnv.RecipeManager.ActivatedRecipe().Name);
Log.I($"[CheckPageNo] Previous Recipe: {previousRecipeInfo.Name}");
// Recipe 활성화
int err = _AuroraEnv.RecipeManager.ActivateRecipe(recipeId, "PLC Request");
if (err != E1.OK) {
Log.E($"[CheckPageNo] Recipe Activation Failed: {err}");
return null;
}
// 이벤트 처리 대기
Thread.Sleep(50);
// 현재 활성화된 Recipe 확인
var currentActivated = _AuroraEnv.RecipeManager.ActivatedRecipe();
Log.I($"[CheckPageNo] Current Activated Recipe: {currentActivated.Id} - {currentActivated.Name}");
// Recipe 로드 및 정보 확인
RecipeHelper.ReloadRecipe(currentActivated.Name);
var currentRecipeInfo = RecipeHelper.getRecipeInfo(currentActivated.Name);
// 더블 체크: 실제로 변경되었는지 확인
if (previousRecipeInfo.Name == currentRecipeInfo.Name) {
Log.W($"[CheckPageNo] Warning: Recipe did not change!");
// Fallback 처리 (재시도, 강제 종료 등)
}
Log.I($"[CheckPageNo] Recipe Successfully Changed: {previousRecipeInfo.Name} → {currentRecipeInfo.Name}");
// 이제 PageNum 읽기 진행
// ...
```
---
## 4. 구체적인 검증 방법
### 4.1 ActivatedRecipe() 메서드로 확인
```csharp
// 활성화 전
var beforeRecipe = _AuroraEnv.RecipeManager.ActivatedRecipe();
Log.I($"Before: {beforeRecipe.Id} - {beforeRecipe.Name}");
// 활성화
int err = _AuroraEnv.RecipeManager.ActivateRecipe(newRecipeId, "Test");
if (err != E1.OK) return null;
Thread.Sleep(50);
// 활성화 후
var afterRecipe = _AuroraEnv.RecipeManager.ActivatedRecipe();
Log.I($"After: {afterRecipe.Id} - {afterRecipe.Name}");
// 검증
if (beforeRecipe.Id != afterRecipe.Id) {
Log.I("✓ Recipe ID changed");
} else {
Log.E("✗ Recipe ID did not change (Possible error)");
}
```
### 4.2 getRecipeInfo() 로 상세 정보 확인
```csharp
// 활성화 후
var recipeInfo = RecipeHelper.getRecipeInfo(afterRecipe.Name);
if (recipeInfo == null) {
Log.E("Failed to load recipe info");
return null;
}
// 주요 필드 검증
Log.I($"Recipe Name: {recipeInfo.Name}");
Log.I($"Layer Count: {recipeInfo.Layers.Count()}");
Log.I($"Total Lines: {recipeInfo.Layers.Sum(l => l.Objects.Count())}");
// Recipe가 제대로 로드되었으면 Canvas에 반영됨
// _SelectedObjectList = recipeInfo.ActiveLayer.Objects;
```
### 4.3 이벤트 핸들러 직접 구독
```csharp
// SeqAutoRunPTOP.Sequence_Enter에서:
_recipeChangeCompleted = false;
_AuroraEnv.EventObservable
.AsObservable(AuroraSysEvent.SYS_RECIPE_ACTIVATED)
.Subscribe(msg => {
Log.I("[Sequence_Enter] SYS_RECIPE_ACTIVATED Event Received");
_recipeChangeCompleted = true;
});
// CheckPageNo에서:
_recipeChangeCompleted = false;
int err = _AuroraEnv.RecipeManager.ActivateRecipe(recipeId, "PLC Request");
if (err != E1.OK) return null;
// 이벤트 대기 (타임아웃 포함)
var startTime = DateTime.Now;
while (!_recipeChangeCompleted && !context.Stopped) {
if (DateTime.Now.Subtract(startTime).TotalSeconds > 5) {
Log.W("[CheckPageNo] Recipe Change Event Timeout");
break;
}
Thread.Sleep(10);
}
Log.I("[CheckPageNo] Recipe Change Event Completed");
```
---
## 5. 정리: 세 가지 검증 질문에 대한 최종 답변
### Q1: ActivateRecipe()가 동기인가?
**답: YES, 동기이다.**
- 반환값(int err)이 있고, 즉시 확인 가능
- 반환 시점에 Recipe 활성화 작업 완료됨
- 하지만 **이벤트 발행 → 이벤트 핸들러 실행**은 다른 스레드/큐를 탈 수 있음
### Q2: EVENT_CHANGE_RECIPE_INFO 발생 타이밍은?
**답: 50ms 지연이 필요하다.**
- ActivateRecipe() 반환 직후 발행되지만,
- 이벤트 핸들러(ReloadRecipe 등)는 비동기로 처리됨
- Dispatcher 큐를 통해 실행되므로 약간의 지연 필요
- **권장: Thread.Sleep(50)** 이상
### Q3: ReloadRecipe()로 변경사항을 검증해야 하는가?
**답: YES, 강력히 권장한다.**
- 이전 Recipe 정보 저장 → getRecipeInfo()
- 활성화 및 로드 → ReloadRecipe()
- 현재 Recipe 정보 확인 → getRecipeInfo()
- 비교 → CompareHelper (RMSHandler 참고)
- 변경사항 검증 → diffs 확인
---
## 6. 다음 단계: 구현 계획 수정
기존 "Phase 1-5"를 다음과 같이 정밀화할 수 있습니다:
### Phase 1: 설정 & 준비
- ProcessRecipeMapping 클래스
- PLC WORD 신호 정의
### Phase 2: 이벤트 구독 설정 (Sequence_Enter)
```csharp
_recipeChangeCompleted = false;
_AuroraEnv.EventObservable
.AsObservable(AuroraSysEvent.SYS_RECIPE_ACTIVATED)
.Subscribe(msg => _recipeChangeCompleted = true);
```
### Phase 3: CheckPageNo 수정
```csharp
// Step 1: 이전 Recipe 정보 저장
var prevRecipe = RecipeHelper.getRecipeInfo(...);
// Step 2: RecipeNum 읽기 & RecipeId 매핑
int recipeId = mapping.GetRecipeId(plcRecipeNum);
// Step 3: Recipe 활성화 (동기)
int err = _AuroraEnv.RecipeManager.ActivateRecipe(recipeId, "PLC Request");
if (err != E1.OK) return null;
// Step 4: 이벤트 처리 대기
Thread.Sleep(50);
// Step 5: 더블 체크 (변경사항 검증)
var currRecipe = RecipeHelper.getRecipeInfo(...);
if (prevRecipe.Name == currRecipe.Name) {
Log.W("Warning: Recipe did not change");
}
// Step 6: PageNum 읽기 (기존 로직)
// ...
```
### Phase 4: 에러 처리
- 타임아웃 처리 (5초)
- Recipe 미존재 처리
- Fallback 로직
---
## 7. 주의사항
1. **Dispatcher.Invoke()의 필요성**
- UI 스레드가 아닌 곳에서 호출 시 필수
- RecipeHelper.ReloadRecipe()는 UI 스레드에서만 안전
2. **Recipe 로드 시간**
- Recipe 크기에 따라 로드 시간 다름
- 복잡한 Recipe는 50ms 이상 필요할 수 있음
- **타임아웃은 최소 5초로 설정 권장**
3. **Rx.NET ObserveOnDispatcher()**
- RMSHandler에서 사용되는 패턴
- UI 스레드 안전성 보장
- SeqAutoRunPTOP에서도 고려
---
**결론:** 당신의 분석 방향이 100% 정확합니다. 코드 수정 전 이 세 가지 검증 항목을 더 깊이 있게 분석하는 것이 매우 현명한 선택입니다.

View File

@ -1,509 +0,0 @@
# Recipe 탭 크래시 분석 보고서 — IO Device DI 읽기 실패
**분석 일시:** 2026-06-16 14:56:23
**대상:** ILSOS_PrismaticRD_PTOP Recipe 탭 시스템 셧다운
**오류:** `System.Exception: ERR_IO_FAIL_TO_GET_BIT; ERROR CODE:3102`
---
## 1. 오류 스택 트레이스 분석
### 1.1 호출 체인
```
┌─────────────────────────────────────────────────────────────────────┐
│ [호출 경로] │
├─────────────────────────────────────────────────────────────────────┤
│ 1. System.Windows.Threading.DispatcherTimer.FireTick() │
│ (타이머 이벤트 발생) │
│ │
│ 2. UCScannerObjectTeachingControlBoxViewModel.OnMonitoring() │
│ 위치: Recipes/UCScannerObjectTeachingControlBoxViewModel.cs:181 │
│ (타이머 콜백 핸들러) │
│ │
│ 3. IODevice.ReadDI(int index) │
│ 위치: AuroraIO/IODevice.cs:212 │
│ (디지털 입력 읽기 메서드) │
│ │
│ 4. AuroraIO.AJINEXTEK.AjinIO.ReadDiBit(int index) │
│ 위치: AuroraIO/Vendor/AJINEXTEK/AjinIO.cs:510 │
│ ❌ 예외 발생 │
│ "ERR_IO_FAIL_TO_GET_BIT; ERROR CODE:3102" │
│ │
│ 5. CAXD.AxdiReadInport(index, ref data) ← 오류의 근원 │
│ (Ajin IO 드라이버 호출 — 하드웨어 통신 실패) │
└─────────────────────────────────────────────────────────────────────┘
```
### 1.2 에러 코드 분석: 3102
**Ajin 모션 컨트롤러 라이브러리 에러 코드 3102:**
| 에러 코드 | 의미 | 가능한 원인 |
|----------|------|-----------|
| **3102** | `AXT_ERR_INVALID_PORT` | ① DI 포트 인덱스 범위 초과<br/>② IO 모듈이 초기화되지 않음<br/>③ IO 포트 설정 불일치<br/>④ 하드웨어 미연결 |
---
## 2. 코드 분석
### 2.1 오류 발생 지점: UCScannerObjectTeachingControlBoxViewModel.cs
```csharp
[Line 171-198]
private void OnMonitoring(object sender, EventArgs e)
{
// Laser Device 파일럿 상태 읽기 (성공)
if (_Laser.GetPilotLaserStatus()) {
IsGuidBeam = true;
} else {
IsGuidBeam = false;
}
// ❌ 이 부분에서 크래시 발생 (Line 181)
if (_IODevice.ReadDI(9) && // ← ERROR CODE:3102 (DI 9 읽기 실패)
_IODevice.ReadDI(24)) // (도달하지 않음)
{
IsLaserReady = true;
} else {
IsLaserReady = false;
}
}
```
**타이머 시작 조건:**
```csharp
[Line 151-159]
private void VisibleChanged(object obj)
{
bool visible = (bool)obj;
if (visible) {
_MonitorTimer.Start(); // ← Recipe 탭이 보여질 때 시작
} else {
_MonitorTimer.Stop();
}
}
[Line 127-130]
_MonitorTimer = new DispatcherTimer();
_MonitorTimer.Tick += OnMonitoring;
_MonitorTimer.Interval = TimeSpan.FromMilliseconds(100); // 100ms마다 OnMonitoring 호출
```
### 2.2 IO 인덱스 읽음
```csharp
[IODevice.cs:212-214]
public bool ReadDI(int index) => _iOUnit.ReadDiBit(index);
public bool ReadDO(int index) => _iOUnit.ReadDoBit(index);
public void WriteDO(int index, bool on) => _iOUnit.WriteDoBit(index, on);
```
### 2.3 Ajin IO 드라이버 호출 (실패 지점)
```csharp
[AjinIO.cs:501-511]
public override bool ReadDiBit(int index)
{
uint data = 0;
// Ajin IO 라이브러리 호출
int err = (int)CAXD.AxdiReadInport(index, ref data);
if (err != (int)AXT_FUNC_RESULT.AXT_RT_SUCCESS) // err = 3102
{
// 예외 발생
throw new Exception(string.Format("ERR_IO_FAIL_TO_GET_BIT;ERROR CODE:{0}", err));
// ↑ 3102
}
return Convert.ToBoolean(data);
}
```
---
## 3. 근본 원인 분석
### 3.1 **DI 인덱스 범위 오류** (가능성 높음)
#### 현상
- 기존 프로젝트 (ILSOS_PrismaticScanner)에서 코드를 copy-paste하여 ILSOS_PrismaticRD_PTOP에 적용
- 기존 프로젝트의 하드웨어 설정과 PTOP의 하드웨어 설정이 다름
#### 의심 지점
**기존 코드 (ILSOS_PrismaticScanner):**
```csharp
// ILSOS_PrismaticScanner의 Recipe 탭 모니터링
_IODevice.ReadDI(9) // Scanner 하드웨어의 DI 포트 9 (유효)
_IODevice.ReadDI(24) // Scanner 하드웨어의 DI 포트 24 (유효)
```
**현재 코드 (ILSOS_PrismaticRD_PTOP):**
```csharp
// PTOP 하드웨어로 복사되었으나 핀 맵이 다름
_IODevice.ReadDI(9) // PTOP 하드웨어에는 DI 9가 없음 또는 다른 목적
_IODevice.ReadDI(24) // PTOP 하드웨어에는 DI 24가 없음 또는 다른 목적
```
#### 검증 방법
**PTOP IO 설정 파일 확인:**
```
ILSOS_PrismaticRD_PTOP/
├── settings/
│ └── IO 설정 파일 (IOConfig.xml 또는 유사)
└── EquipDef.cs ← DI/DO 인덱스 정의 확인
```
**EquipDef.cs에서 DI 9, 24의 정의 확인:**
```csharp
// 있어야 함:
public const int DI_SOMETHING = 9; // 무엇을 읽는가?
public const int DI_LASER_READY = 24; // 또는 다른 용도?
// 없을 수도 있음 → 인덱스 미스매치
```
---
### 3.2 **IO 모듈 미초기화** (가능성 중간)
#### 현상
Recipe 탭 진입 시점에 IO 모듈이 완전히 초기화되지 않음
#### 의심 지점
**IODevice 초기화 순서:**
```csharp
[IODevice.cs:146-198 DoActivate]
protected override int DoActivate(IDeviceConfig config)
{
// ... 코드 생략 ...
int nRet = _iOUnit.Open(m_IOUnitConfig.DIDOIP); // Step 1: 포트 오픈
if (nRet != 0)
return nRet;
LoadIOTable(); // Step 2: IO 테이블 로드
nRet = _iOUnit.Initialize(); // Step 3: 초기화
// 이 시점에서 Recipe 탭이 활성화되면?
// → OnMonitoring이 즉시 호출될 수 있음
// → IO 테이블 로드 전에 DI 읽기 시도 가능
}
```
**EquipmentSetup에서 IODevice 활성화 타이밍:**
```
1. Application 시작
2. EquipmentSetup.Activate() 호출
├─ 각 Device 순차 활성화
├─ IODevice 활성화 (DoActivate 호출)
│ └─ LoadIOTable() → _iOUnit.Initialize()
│ (이 순간 IO 포트들이 설정됨)
│
└─ View/ViewModel 초기화
└─ Recipe 탭 ViewModel 생성
└─ _MonitorTimer 등록 (하지만 아직 시작 안 함)
3. UI 표시
└─ Recipe 탭 클릭 → VisibleChanged(true)
└─ _MonitorTimer.Start() ❌ 크래시 (IO 미초기화 상태)
```
---
### 3.3 **하드웨어 미연결** (가능성 낮음)
IO 모듈이 PC에 연결되지 않은 경우, 모든 DI 읽기가 실패합니다.
---
## 4. 코드 추적: 기존 프로젝트와의 차이
### 4.1 ILSOS_PrismaticScanner vs ILSOS_PrismaticRD_PTOP
#### ILSOS_PrismaticScanner (정상 작동)
```
ILSOS_PrismaticScanner/
├── EquipDef.cs
│ └─ DI 9, 24 정의됨 ✓
├── Recipes/
│ └─ UCScannerObjectTeachingControlBoxViewModel.cs
│ └─ OnMonitoring()에서 DI 9, 24 읽음 ✓
└── settings/
└─ IOConfig.xml (또는 유사)
└─ DI 포트 매핑 9, 24 정의됨 ✓
```
#### ILSOS_PrismaticRD_PTOP (오류 발생)
```
ILSOS_PrismaticRD_PTOP/
├── EquipDef.cs
│ └─ DI 9, 24 정의? ??? (확인 필요)
├── Recipes/
│ └─ UCScannerObjectTeachingControlBoxViewModel.cs (복사됨)
│ └─ OnMonitoring()에서 DI 9, 24 읽음 ❌ (PTOP 하드웨어에 없음)
└── settings/
└─ IOConfig.xml 또는 설정 파일
└─ DI 포트 매핑 정의? ??? (PTOP별 설정 필요)
```
### 4.2 같은 코드, 다른 하드웨어
**비전 시스템의 Laser Ready 신호를 읽는 로직:**
- **Scanner**: Laser 보기 헤드에서 발생하는 신호
- Pilot Laser 상태 (위치 확인용)
- Laser Ready 신호 (발사 가능 여부)
- **PTOP (Prismatic Rectangular)**: 배터리 각형 용접
- 고정 레이저 (이동하지 않음)
- Stage A / Stage B 교번 스캔
- Laser Ready 신호의 의미가 다를 수 있음
---
## 5. 해결 방안
### **방안 A: DI 인덱스 검증 및 수정** (권장)
**Step 1: PTOP의 실제 IO 설정 확인**
```bash
# 1. PTOP의 IO 설정 파일 확인
ILSOS_PrismaticRD_PTOP/settings/ 폴더에서
IOConfig, IO.xml, 또는 유사 파일 검색
# 2. PTOP의 EquipDef.cs 확인
상수로 정의된 DI/DO 인덱스 확인:
예:
public const int DI_SOMETHING = X;
public const int DI_LASER_READY = Y;
# 3. 하드웨어 사양서 확인
PTOP 머신의 IO 포트 매핑 문서
→ DI 9, 24가 실제로 존재하는지 확인
```
**Step 2: 올바른 DI 인덱스로 수정**
만약 PTOP의 Laser Ready 신호가 DI 9, 24가 아니라면:
```csharp
// 수정 전 (잘못된 코드)
if (_IODevice.ReadDI(9) && _IODevice.ReadDI(24))
// 수정 후 (PTOP의 실제 DI 인덱스 사용)
// 예: PTOP의 Laser Ready가 DI 5, 15라면
if (_IODevice.ReadDI(5) && _IODevice.ReadDI(15))
// 또는 EquipDef에서 상수 사용
if (_IODevice.ReadDI(CommonDeviceIds.DI_LASER_READY_1) &&
_IODevice.ReadDI(CommonDeviceIds.DI_LASER_READY_2))
```
**Step 3: Recipe 탭 모니터링 로직 검증**
Recipe 탭의 Laser Ready 상태가 PTOP에 필요한가?
- 필요하면 → 올바른 DI 인덱스 사용
- 불필요하면 → 모니터링 로직 제거 또는 비활성화
```csharp
// 방안 A-1: 불필요하면 모니터링 제거
private void OnMonitoring(object sender, EventArgs e)
{
// PTOP에서는 Laser Ready 상태를 Recipe 탭에서 읽을 필요 없음
// OnMonitoring 전체 로직 제거 또는 주석 처리
}
// 방안 A-2: 오류 처리 추가 (임시)
private void OnMonitoring(object sender, EventArgs e)
{
try
{
if (_IODevice.ReadDI(9) && _IODevice.ReadDI(24))
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
catch (Exception ex)
{
Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}");
IsLaserReady = false; // 기본값으로 설정
}
}
```
---
### **방안 B: 조건부 모니터링** (안정성 향상)
PTOP과 Scanner 중 어느 것인지를 런타임에 판별하여 모니터링 수행:
```csharp
private void OnMonitoring(object sender, EventArgs e)
{
// PTOP인지 Scanner인지 판별
bool isPTOP = _SystemParameter.IsEquipmentType("PTOP");
if (isPTOP)
{
// PTOP: 모니터링 스킵 또는 PTOP별 로직
IsLaserReady = true; // 또는 PTOP의 실제 신호 읽기
}
else
{
// Scanner: 기존 로직 유지
if (_IODevice.ReadDI(9) && _IODevice.ReadDI(24))
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
}
```
---
### **방안 C: IO 초기화 대기** (근본 원인이 미초기화인 경우)
```csharp
private void VisibleChanged(object obj)
{
bool visible = (bool)obj;
if (visible)
{
// IO 모듈이 초기화될 때까지 대기
if (_IODevice._iOUnit.IsConnected())
{
_MonitorTimer.Start();
}
else
{
Log.W("[Recipe Tab] IO 모듈이 아직 초기화되지 않음. 모니터링 지연");
// 100ms 후 재시도
Task.Delay(100).ContinueWith(_ =>
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
if (_IODevice._iOUnit.IsConnected())
{
_MonitorTimer.Start();
}
});
});
}
}
else
{
_MonitorTimer.Stop();
}
}
```
---
## 6. 즉시 조치사항
### **우선순위 1: DI 인덱스 확인** (1시간)
```bash
ILSOS_PrismaticRD_PTOP 프로젝트에서:
1. EquipDef.cs 열기 → DI 상수 정의 확인
2. settings/ 폴더에서 IO 설정 파일 찾기
3. PTOP 하드웨어 사양서 또는 설정 문서 확인
→ DI 9, 24이 PTOP에 실제로 존재하는가?
```
### **우선순위 2: Laser Ready 신호의 필요성 확인** (30분)
```bash
Recipe 탭에서 Laser Ready 상태가 필요한가?
1. ILSOS_PrismaticScanner와 ILSOS_PrismaticRD_PTOP의
레이저 시스템 비교
2. PTOP에서 Laser Ready가 필요하면:
→ 올바른 DI 인덱스 찾기
필요 없으면:
→ OnMonitoring 로직 제거
```
### **우선순위 3: 임시 오류 처리** (30분)
```csharp
// OnMonitoring에 try-catch 추가 (방안 A-2)
// → 크래시 방지 + 로그 확인 용이
// → 근본 원인 파악 후 제거할 것
```
---
## 7. 검증 포인트
### 7.1 수정 후 테스트
1. **Recipe 탭 진입 테스트**
```
1. 애플리케이션 시작
2. 초기화 완료 확인
3. Recipe 탭 클릭
4. 시스템 셧다운 여부 확인 ✓
```
2. **로그 확인**
```
OnMonitoring에서 DI 값을 로깅하여 실제로 읽어지는지 확인:
Log.I($"[OnMonitoring] DI(9)={_IODevice.ReadDI(9)}, DI(24)={_IODevice.ReadDI(24)}");
```
3. **IO 하드웨어 상태 확인**
```
IODevice 초기화 시 IsConnected() 상태 로깅:
Log.I($"[IODevice] IsConnected={_IODevice._iOUnit.IsConnected()}");
```
---
## 8. 참고: Laser Ready 신호 아키텍처
### Scanner vs PTOP 비교
| 항목 | ILSOS_PrismaticScanner | ILSOS_PrismaticRD_PTOP |
|------|------------------------|----------------------|
| **레이저 타입** | 스캐너 (이동) | 고정 + Stage 교번 |
| **Pilot Laser** | 위치 확인용 신호 있음 | 고정이므로 필요성 낮음 |
| **Laser Ready DI** | DI 9, 24 | ? (확인 필요) |
| **Recipe 탭 용도** | 레이저 상태 모니터링 | ? (확인 필요) |
---
## 9. 결론
| 단계 | 조치 | 예상 시간 |
|------|------|---------|
| **1. 근본 원인 확인** | DI 인덱스 + IO 초기화 상태 확인 | 1시간 |
| **2. 임시 조치** | try-catch 오류 처리 추가 | 30분 |
| **3. 근본 해결** | 올바른 DI 인덱스로 수정 또는 모니터링 제거 | 1시간 |
| **4. 테스트** | Recipe 탭 진입 테스트 + IO 로깅 확인 | 1시간 |
---
**작성자:** Claude Code Analysis
**참고 파일:**
- `UCScannerObjectTeachingControlBoxViewModel.cs:171-198` — OnMonitoring 메서드
- `IODevice.cs:212-214` — ReadDI 메서드
- `AjinIO.cs:501-511` — ReadDiBit 구현
- 오류 로그: `sys_error/20260616_103211.148.log`

View File

@ -1,187 +0,0 @@
# Recipe View Laser Ready (DI 9) — IsLaserReady 조건 분석
**분석 일시:** 2026-06-17 14:31:31
**대상:** `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs`
**비교 대상:** `docs/기존파일비교/UCScannerObjectTeachingControlBoxViewModel.cs` (이전 MOTF 프로젝트 버전)
---
## 0. 요청 내용 (사용자)
- 현장 RTC5보드는 갈보모터 X, Y 피드백값을 가져오는 게 없어서 DEVICE 상에서 COMMAND 값을 불러오도록 체크 해제 후 진행. 정확한 피드백값은 확인 불가한 상태.
- Laser 동작은 I/O로 하며, 프로그램 내에서 사용할 때 Simulation으로 동작한다.
- Recipe View에서 Laser Ready 부분이 DI(9)를 보고 있는데, 해당 BIT가 Laser를 IO On 해도 번들을 켜도 안 켜져서 전선 다시 확인 중인 상태. simulation이어서 true가 되긴 해야 하지만 한번 확인해볼 것. 이전 MOTF 작업하던 프로젝트랑 비교하기.
- **요청 1:** 현재 프로젝트의 `IsLaserReady` 변수 조건과 `docs/기존파일비교`의 같은 파일명 버전을 비교해서 달라진 점 분석.
- **요청 2:** IO(9)를 보고 있는 모든 코드 위치 파악.
- **요청 3:** 시뮬일 경우 버튼을 누르면 IsLaserReady 상태값이 변경되도록 하는 방안 검토 (DI(9)가 TRUE가 되지 않아 테스트가 막힘). — *코드 수정은 보류, 방안만 정리.*
---
## 1. 결론 요약
- 점등이 안 되는 직접 원인은 **IsLaserReady 판정 소스가 바뀐 것**:
- 기존(MOTF): `_Laser.GetLaserReady()` (Laser 디바이스 상태, simulation이면 true)
- 현재: `_IODevice.ReadDI(9)` (**실제 하드웨어 DI 9번 비트**)
- 현재 코드는 시뮬레이션 Laser 디바이스가 아니라 **물리 DI(9)** 를 읽으므로, 배선으로 DI(9)가 실제로 들어오지 않으면 simulation이어도 true가 되지 않음.
- 작업일지에 적은 "simulation이면 true여야 한다"는 **기존 코드 기준**이며, 현재 코드는 그렇게 동작하지 않음.
- DI(9)는 "레이저 Ready 출력 → 장비 입력" 신호이므로 **OnMonitoring이 DI(9)를 읽는 것 자체는 설계상 정상.** 점등 미동작은 코드가 아니라 **레이저 Ready 출력 → DI(9) 결선** 문제 → 전선 확인이 맞는 방향.
---
## 2. 코드 비교 — IsLaserReady 판정부
### 2.1 OnMonitoring()
**기존(MOTF):**
```csharp
if (_Laser.GetLaserReady()) {
IsLaserReady = true;
} else {
IsLaserReady = false;
}
```
**현재:**
```csharp
try
{
if (_IODevice.ReadDI(9)) // ← 물리 DI 9번을 직접 읽음
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
catch (Exception ex)
{
Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}");
IsLaserReady = false;
}
```
### 2.2 PerformCmdLaserReadyCheck() — Ready Check 버튼
**기존(MOTF):**
```csharp
if (!_Laser.GetLaserReady()) {
_Laser.LaserOn(); // 실질적으로 Laser Ready On
} else {
_Laser.LaserOff();
}
```
**현재:**
```csharp
if(toggleIO)
{
_IODevice._iOUnit.WriteDoBit(9, true); // ← DO 9 출력
_IODevice._iOUnit.WriteDoBit(13, true); // ← DO 13 출력
toggleIO = false;
}
else
{
_IODevice._iOUnit.WriteDoBit(9, false);
_IODevice._iOUnit.WriteDoBit(13, false);
toggleIO = true;
}
```
### 2.3 부수 차이 (동작엔 직접 영향 적음)
- 필드 선언: 기존 `private readonly IODevice _IODevice;` → 현재 `private IODevice _IODevice;` (readonly 제거). 추가 필드 `public bool toggleIO = true;`.
- using 추가: `WeldingCommon`, `WeldingCommon.Devices.Laser`, `using static ...PAIX_PrismaticScanner`.
- 기존 OnMonitoring의 LaserOnLamp `WriteDoBit` 주석 블록은 현재 제거됨.
- `PerformCmdRunTeachingRun`: 기존은 `PopupSequenceDiagram(...)` 호출, 현재는 주석 처리. `OnCompleteTeachingRun`도 기존은 성공 처리/`ShowWarningToast` 활성, 현재는 주석 처리.
---
## 3. 채널 충돌 — 버튼(DO9) vs 모니터링(DI9)
```
[버튼] WriteDoBit(9,true) ──▶ DO 9 (출력)
│ ?? 물리 배선 필요 ??
[모니터] ReadDI(9) ◀─────────── DI 9 (입력)
```
- 버튼이 쓰는 것은 **DO9/DO13 출력**, 모니터링이 읽는 것은 **DI9 입력** → 별개 채널.
- DO9에 써도 DI9 입력으로 되돌아오려면 물리 배선(루프백/외부 결선)이 필요. 이 결선이 없으면 버튼을 눌러도 IsLaserReady가 안 켜짐 → 지금 전선 확인 중인 이슈와 일치.
---
## 4. IO(9)를 보고 있는 모든 코드 위치
리터럴 `9`로 IO 9번에 접근하는 코드는 **한 파일, 3곳뿐**:
`WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs`
| 라인 | 코드 | 종류 | 의미 |
|---|---|---|---|
| 182 | `if (_IODevice.ReadDI(9))` | DI 9 읽기 | Laser Ready 판정 (모니터링) |
| 462 | `_IODevice._iOUnit.WriteDoBit(9, true)` | DO 9 쓰기 | Ready Check 버튼 ON |
| 468 | `_IODevice._iOUnit.WriteDoBit(9, false)` | DO 9 쓰기 | Ready Check 버튼 OFF |
> 같은 버튼이 인접 라인에서 `WriteDoBit(13, …)`도 함께 씀.
### enum 매핑상 9번의 실제 의미 (`AuroraDevices/AuroraIO/Vendor/PAIX/PAIXIO.cs`)
DI와 DO는 별개이고, 9번이 가리키는 게 서로 다름:
```
ePDIAddress (입력) ePDOAddress (출력)
DoorLockOpen4 = 7 WorkLEDOn = 7
(8 - 미지정/미사용) (8 - 미지정)
(9 - 미지정/미사용) ◀DI9 ResetSwitchLamp = 9 ◀DO9
DoorLockCloseMonitoring=10 ...
ModeKeySolOn = 13 ◀DO13
```
- **DI 9번**: `ePDIAddress`에 이름이 없는 raw 비트 (Laser Ready 정식 주소로 매핑돼 있지 않음)
- **DO 9번**: `ResetSwitchLamp` (리셋 스위치 램프)
- **DO 13번**: `ModeKeySolOn` (모드키 솔레노이드)
- → 버튼이 켜는 DO9/13은 enum상 레이저와 무관한 비트이며, DI9도 미매핑 raw 입력. 코드 레벨에서 임시 매핑된 자리.
- DO9(`ResetSwitchLamp`)·DO13(`ModeKeySolOn`)을 enum 이름으로 호출하는 코드는 없음 → IO 9번 관련 코드는 위 3줄이 전부.
---
## 5. 시뮬일 때 버튼으로 IsLaserReady 토글하는 방안 (수정 보류 — 방안만)
선택안: **버튼이 IsLaserReady를 직접 토글.** 단, `OnMonitoring()`이 100ms마다 `ReadDI(9)`로 덮어쓰므로 시뮬일 때 모니터링 덮어쓰기를 같이 막아야 함.
- 시뮬 판별은 `_IODevice.IsSimul` 로 가능 (Device 기반 public 속성, 외부 사용 예: `SeqAlarmMonitor.cs:466` `_IO.IsSimul`).
**(1) `OnMonitoring()` — 시뮬일 때 DI(9) 읽기 건너뛰기**
```csharp
if (!_IODevice.IsSimul)
{
try
{
if (_IODevice.ReadDI(9)) { IsLaserReady = true; }
else { IsLaserReady = false; }
}
catch (Exception ex) { Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}"); IsLaserReady = false; }
}
```
**(2) `PerformCmdLaserReadyCheck()` — 시뮬일 때 직접 토글**
```csharp
if (_IODevice.IsSimul)
{
IsLaserReady = !IsLaserReady;
return;
}
// 실장: 기존 DO9/13 Write 로직 그대로
```
- 시뮬: 버튼 → IsLaserReady 토글, 모니터링 미관여 → 점등 확인 가능
- 현장: 기존 로직 그대로 (DI9 읽기 / DO9·13 Write) — 변경 없음
> **상태:** 위 방안은 검토만 완료. 실제 코드 수정은 적용하지 않음 (사용자 보류 요청).
---
## 6. 확인/해결 방향
- DI(9) 판정을 유지하려면 **레이저 Ready 출력 → DI(9) 물리 결선**을 확인할 것 (전선 점검이 맞는 방향).
- 시뮬레이션에서 동작 확인이 목적이면 5절 방안 적용을 검토.
- 버튼이 출력하는 DO9(`ResetSwitchLamp`)·DO13(`ModeKeySolOn`)이 실제 레이저 Enable 배선과 맞는지 확인 후, 맞지 않으면 버튼 동작을 의미에 맞게 재정의 필요.

View File

@ -1,162 +0,0 @@
# PLC 전축 위치값 모니터링 InterLock — Setting 추가 위치 분석/추천
**분석 일시:** 2026-06-17 14:39:00
**대상:** PTOP 프로젝트 Settings 구조
**목표:** PLC에서 받는 전축(全軸) 위치값을 모니터링하기 위한 기준값(범위)을 Setting에 미리 저장하고, 특정 범위를 벗어나면 InterLock을 거는 기능. 저장할 Setting 클래스/위치/그룹명 추천.
---
## 0. 요청 내용 (사용자)
- PLC에서 전축 위치값을 모니터링할 값을 미리 Setting에 저장해두고, 특정 범위를 벗어나면 InterLock을 걸려고 함.
- Settings에 여러 Setting 클래스들이 있는데 **어느 클래스 어느 위치에 추가**하는 게 좋을지 추천.
- 적당한 위치가 없으면 **어느 클래스에 어떤 그룹명**으로 추가하면 되는지도 추천해서 docs 분석에 기록.
---
## 1. 결론 요약
- **기존 클래스 중 "딱 맞는" 자리는 없음.** 전축 위치값의 범위(Min/Max) InterLock은 *축마다 반복되는 구조화 데이터*라, 평면(scalar) 속성 위주인 `SystemParameter`/`SequenceSetting`에 그대로 넣기엔 부적합.
- **추천(권장): 신규 전용 Setting 클래스 신설.**
- 클래스: `AxisPositionInterlockSetting : ISettingItem`
- 위치: `WeldingComponents/WeldingCommon/Settings/AxisPositionInterlockSetting.cs`
- 등록 ID: `CommonSettingIds.AxisPositionInterlock = "AxisPositionInterlock"` (EquipDefCommon.cs)
- 등록: `CommonEquipmentSetup.RegisterSettings()`에 `register(CommonSettingIds.AxisPositionInterlock, new AxisPositionInterlockSetting());`
- 그룹명(Category): `"Axis Position Interlock"`
- 축별 항목 구조는 기존 `TeachingPositionSetting.SettingPosition`(PrismaticGun) 패턴을 차용.
- **간이 대안(축 수가 적고 빠른 적용이 목적):** `SequenceSetting`에 `[Category("Axis Position Interlock")]` 그룹으로 전역 On/Off + 축별 Min/Max scalar 추가. 단, 축 수 확장에 약함.
---
## 2. 현재 Settings 구조 (확인 결과)
### 2.1 Setting 클래스 패턴
- 모든 Setting은 `Aurora.Api.ISettingItem` 구현.
- 프로퍼티에 `PropertyTools.DataAnnotations`의 `[Category("그룹명")]`, `[DisplayName(...)]`, `[Browsable]`, `[ReadOnly]` 부착 → Setting UI에 그룹/표시명으로 노출.
### 2.2 등록 흐름
- ID 상수: `WeldingComponents/WeldingCommon/EquipDefCommon.cs` 의 `class CommonSettingIds`
```csharp
public const string SystemParameter = "SystemParameterILSOS";
public const string DoorInterlock = "DoorInterlock";
public const string Sequence = "Sequence";
// ...
```
- 인스턴스 등록: `WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs` 의 `RegisterSettings(SettingRegister register)`
```csharp
register(CommonSettingIds.SystemParameter, new SystemParameter());
register(CommonSettingIds.LaserPowerCalParam, new LaserPowerCalParam());
register(CommonSettingIds.Sequence, new SequenceSetting());
register(CommonSettingIds.DoorInterlock, new DoorInterlockSetting());
register(CommonSettingIds.RmsDatas, new SettingRmsDataList());
```
- 소비(읽기): 시퀀스/뷰에서 `AuroraEnv.SettingEntry[CommonSettingIds.XXX] as XXX`,
모니터 시퀀스에서는 `context.Settings[CommonSettingIds.Sequence] as SequenceSetting` (예: `SeqStatusMonitor.StartNode`).
### 2.3 후보 클래스 평가
| 클래스 | 위치 | 성격 | 전축 위치 InterLock 적합성 |
|---|---|---|---|
| `SystemParameter` | WeldingCommon/Settings | 전역 평면 파라미터 다수, `[Category]` 그룹 많음 | 축별 반복 구조엔 부적합. 단일 전역 토글 정도만 가능 |
| `SequenceSetting` | WeldingCommon/Settings | 모니터링 On/Off 토글 모음("Alarm Monitoring Device" 등), 모니터 시퀀스가 직접 읽음 | **전역 Enable 토글** 자리로는 적합. 축별 Min/Max 보관엔 부적합 |
| `DoorInterlockSetting` | WeldingCommon/Views/DoorInterlock | Door/Motor 인터락 전용(비번/해제 등) | 의미는 "인터락"으로 가깝지만 Door 도메인 전용 → 혼입 비권장 |
| `TeachingPositionSetting` | **PrismaticGun**/settings | 축별 X/Y/Z 위치 + ±Limit 배열(`SettingPosition[]`) | **구조 선례로 가장 유사**. 단 PrismaticGun 전용이라 PTOP에서 그대로 못 씀 → 패턴만 차용 |
> 즉, "축별 범위" 구조의 정확한 선례는 `TeachingPositionSetting` 하나뿐이고, PTOP 공통 영역에는 대응 클래스가 없음.
### 2.4 PLC 전축 위치값 매핑 현황
- `Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs` 의 `eWordInput`은
Cell/Lot ID·Page·Step·Slot 위주이고 **축 위치값 워드는 아직 미정의(대부분 `EMPTY_xx`)**.
- 따라서 본 기능은 (1) Setting 신설 + (2) **PLC `eWordInput`에 축 위치 워드 주소 매핑 추가**가 함께 필요.
- 위치값 읽기는 `PLCController.Logic.cs`의 `ReadWord(eWordInput)` / `ReadWords(start,end)` 사용.
---
## 3. 권장안 — 신규 전용 Setting 클래스
### 3.1 클래스 (스케치)
`WeldingComponents/WeldingCommon/Settings/AxisPositionInterlockSetting.cs`
```csharp
namespace WeldingCommon.Settings
{
public class AxisPositionInterlockSetting : ISettingItem
{
[Category("Axis Position Interlock")]
[DisplayName("전축 위치 InterLock 사용")]
public bool UseAxisPositionInterlock { get; set; } = false;
// 축별 범위 항목 (TeachingPositionSetting.SettingPosition 패턴 차용)
public AxisInterlockItem[] Axes { get; set; }
public AxisPositionInterlockSetting()
{
// 전축 정의: 장비 축 개수에 맞춰 초기화
Axes = new[]
{
new AxisInterlockItem("AxisX"),
new AxisInterlockItem("AxisY"),
new AxisInterlockItem("AxisZ"),
// ... 전축
};
}
}
public class AxisInterlockItem : NotifyPropertyChangedBase
{
public string Name { get; set; }
public bool Use { get; set; } = true; // 축별 On/Off
public double Min { get; set; } // 하한
public double Max { get; set; } // 상한
// 필요 시: PLC 워드 주소 인덱스, AlarmLevel(Heavy/Light) 등 추가
public AxisInterlockItem() { }
public AxisInterlockItem(string name) { Name = name; }
}
}
```
### 3.2 ID 상수 추가
`WeldingComponents/WeldingCommon/EquipDefCommon.cs` → `class CommonSettingIds`
```csharp
public const string AxisPositionInterlock = "AxisPositionInterlock";
```
### 3.3 등록
`WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs` → `RegisterSettings()`
```csharp
register(CommonSettingIds.AxisPositionInterlock, new AxisPositionInterlockSetting());
```
### 3.4 InterLock 판정 로직 위치
- 모니터 시퀀스에서 주기적으로 PLC 축 위치를 읽어 Min/Max 비교 → 벗어나면 InterLock/알람.
- 권장 위치: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/Monitoring/SeqStatusMonitor.cs` (또는 `SeqAlarmMonitor.cs`).
- `StartNode`에서 `context.Settings[CommonSettingIds.AxisPositionInterlock] as AxisPositionInterlockSetting` 캐싱.
- `UpdateStatus`/`UpdateIO` 루프에서 `ReadWord(eWordInput.축위치)` ↔ `Axes[i].Min/Max` 비교.
- 위반 시 `AlarmManager`로 알람 발생 + 필요 시 `_plc.WriteBit(...)`로 인터락 신호 출력(기존 `PC_HEAVY_ALARM_STATUS` 패턴 참조).
---
## 4. 간이 대안 (빠른 적용용)
축 수가 적고 구조화가 과하다면, `SequenceSetting`에 그룹만 추가:
```csharp
[Category("Axis Position Interlock")]
public bool UseAxisPositionInterlock { get; set; } = false;
public double AxisXMin { get; set; }
public double AxisXMax { get; set; }
public double AxisYMin { get; set; }
public double AxisYMax { get; set; }
// ...
```
- 장점: 신규 클래스/등록 불필요(이미 모니터가 `CommonSettingIds.Sequence`로 읽음).
- 단점: 축 수 확장/가독성 약함. "SequenceSetting 변경 시 재부팅 필요"(클래스 상단 주석) 제약 있음.
---
## 5. 최종 추천
1. **권장:** 신규 `AxisPositionInterlockSetting`(WeldingCommon/Settings) + `CommonSettingIds.AxisPositionInterlock` + `CommonEquipmentSetup.RegisterSettings()` 등록, 그룹명 **"Axis Position Interlock"**, 축별 항목은 `TeachingPositionSetting.SettingPosition` 패턴 차용.
2. 함께 필요: PLC `eWordInput`에 전축 위치 워드 주소 매핑 추가.
3. 판정/인터락은 `SeqStatusMonitor`(또는 `SeqAlarmMonitor`)에서 Setting을 읽어 Min/Max 비교 후 `AlarmManager`/`WriteBit`로 처리.
4. 빠른 PoC가 목적이면 4절 간이 대안으로 `SequenceSetting`에 "Axis Position Interlock" 그룹 추가도 가능(확장성은 낮음).
> 상태: 위치/구조 추천만 정리. 실제 코드 추가는 미적용.

View File

@ -1,86 +0,0 @@
# SequenceSetting "Axis Position Interlock" 그룹 추가 — 속성 설계 분석
**분석 일시:** 2026-06-17 14:44:42
**대상 소스:** `WeldingComponents/WeldingCommon/Settings/SequenceSetting.cs` (`SequenceSetting` 클래스)
**관련 분석:** `docs/분석/2026/06/17/143900_axis_position_interlock_setting_location.md` (간이 대안 절)
**목표:** 전축 위치(섹터 각도) 모니터링 InterLock 기준값을 `SequenceSetting`에 그룹으로 추가.
---
## 0. 확정 조건 (사용자)
- **위치:** `SequenceSetting`에 `[Category("Axis Position Interlock")]` 그룹만 추가 (신규 클래스 신설 안 함).
- **속성:** 수직값 / 수평값을 **단일 기준값 + 허용오차** 형태로 등록 (Min/Max 아님).
- **단위:** 섹터값이라 **도(°, degree)**.
- **Enable 토글:** 추가 안 함 (`UseAxisPositionInterlock` 없음).
- **상태:** 코드 수정은 미적용. 분석/설계만 기록.
---
## 1. 추가 위치 (소스)
- 파일: `WeldingComponents/WeldingCommon/Settings/SequenceSetting.cs`
- 클래스: `public class SequenceSetting : ISettingItem`
- 추가 지점: 클래스 마지막 속성 `GroupRamingUse`(파일 기준 약 L69) **아래**에 신규 그룹 추가.
- 등록은 이미 되어 있음 — `CommonEquipmentSetup.RegisterSettings()`에서
`register(CommonSettingIds.Sequence, new SequenceSetting());` (추가 등록 불필요).
- 소비(읽기): 모니터 시퀀스에서 `context.Settings[CommonSettingIds.Sequence] as SequenceSetting`
(예: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/Monitoring/SeqStatusMonitor.cs` `StartNode`).
---
## 2. 제안 속성 (스케치 — 미적용)
```csharp
[Category("Axis Position Interlock")]
[DisplayName("수직 기준값 [deg]")]
public double VerticalSector { get; set; } = 0.0;
[DisplayName("수직 허용오차 [deg]")]
public double VerticalTolerance { get; set; } = 0.0;
[DisplayName("수평 기준값 [deg]")]
public double HorizontalSector { get; set; } = 0.0;
[DisplayName("수평 허용오차 [deg]")]
public double HorizontalTolerance { get; set; } = 0.0;
```
> 속성명(`VerticalSector` / `HorizontalSector` 등)은 제안값. 명명 선호에 따라 조정 가능.
---
## 3. 판정 로직 (참고)
기준값 ± 허용오차 범위로 InterLock 판정:
```
하한 = VerticalSector - VerticalTolerance
상한 = VerticalSector + VerticalTolerance
if (현재수직각도 < 하한 || 현재수직각도 > 상한) → InterLock
// 수평도 동일하게 HorizontalSector ± HorizontalTolerance
```
- 위치: `SeqStatusMonitor`(또는 `SeqAlarmMonitor`)의 루프 노드에서 PLC 각도값과 비교.
- 위반 시 `AlarmManager`로 알람 발생 + 필요 시 `_plc.WriteBit(...)` 인터록 신호
(기존 `PC_HEAVY_ALARM_STATUS` 출력 패턴 참조).
---
## 4. 주의점
1. **재부팅 필요** — `SequenceSetting.cs` 상단 주석 *"SequenceSetting 이 변경되면, 재부팅 필요함."*
→ 이 그룹 값을 변경하면 재부팅이 전제됨.
2. **PLC 각도 워드 미매핑** — `Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs`의
`eWordInput`에 전축(수직/수평) 섹터 각도 워드 주소가 아직 없음(대부분 `EMPTY_xx`).
→ 실제 모니터링하려면 워드 주소 매핑 추가가 선행돼야 함. 읽기는 `PLCController.Logic.cs`의
`ReadWord(eWordInput)` 사용.
---
## 5. 적용 시 체크리스트 (요약)
- [ ] `SequenceSetting.cs` 에 `Axis Position Interlock` 그룹 4속성 추가 (수직/수평 × 기준값/허용오차, 단위 deg)
- [ ] PLC `eWordInput` 에 수직/수평 섹터 각도 워드 주소 매핑
- [ ] `SeqStatusMonitor`(또는 `SeqAlarmMonitor`)에 기준값±허용오차 판정 + 알람/인터록 처리
- [ ] 설정 변경 후 재부팅 안내(설계상 제약)
> 상태: 설계/위치 분석만. 실제 코드 추가는 미적용.

View File

@ -1,122 +0,0 @@
# Laser Ready 판정 변경 — DI(9) → DI(12) & DI(24) 동시 조건
**분석 일시:** 2026-06-17 15:00:31 (15:10 정확한 매핑으로 갱신)
**대상:** `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBoxViewModel.cs`
**관련:** `docs/분석/2026/06/17/143131_laser_ready_di9_analysis.md` (이전 DI9 분석)
---
## 0. 요청/설명 (사용자)
- 토글 버튼(`PerformCmdLaserReadyCheck`)을 누르면 **Control (9, 13)** 이 켜지고 꺼짐 (현행 `WriteDoBit(9)`, `WriteDoBit(13)` 유지).
- `OnMonitoring`에서는 현재 **DI(9)만** 보고 있는데, 9번이 아니라 **12번과 24번 둘 다(AND)** 보는 조건으로 변경.
- 확정 사항: **(1) AND 조건 맞음. (2) DI 24는 현장 배선 기준이 맞음(아래 enum 불일치 참고).**
---
## 1. 매핑 정정 — 진짜 enum은 PAIXIO가 아니라 PAIX_PrismaticScanner
처음엔 범용 `AuroraIO/Vendor/PAIX/PAIXIO.cs`의 `ePDIAddress`(DI12=StartSwitch, DI24=FanAlarm8)로 봤으나,
이 ViewModel은 상단에 `using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner;` 가 있고
**레이저 전용 IO 매핑**(`WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs` 내 `PAIX_PrismaticScanner`)을 사용함.
머신측 `Machines/ILSOS_PrismaticRD_PTOP/Device/IO/PAIX_PrismaticScanner.cs` 도 동일 enum.
### 1.1 입력 `ePrismaticDIAddress`
| DI | 의미 |
|---|---|
| 9 | CoreLaser_Laser_Ready |
| 10 | CoreLaser_Error_State |
| 11 | CoreLaser_Laser_Output_State |
| **12** | **CoreLaser_Control_Status** |
| 13 | RingLaser_Laser_Ready |
| 14 | RingLaser_Error_State |
| 15 | RingLaser_Laser_Output_State |
| 23(enum) / **24(현장)** | **RingLaser_Control_Status** |
### 1.2 출력 `ePrismaticDOAddress`
| DO | 의미 |
|---|---|
| 9 | CoreLaser_Control |
| 10 | CoreLaser_ErrorClearing |
| 13 | RingLaser_Control |
| 14 | RingLaser_ErrorClearing |
| 17 | EX_Red_Control |
### 1.3 의미 해석
- 토글 버튼: **DO9 = CoreLaser_Control**, **DO13 = RingLaser_Control** → Core·Ring 레이저 Control 출력 ON/OFF.
- 변경 후 모니터링: 그 피드백인 **Control_Status** 입력 2개를 봄
- **DI12 = CoreLaser_Control_Status**
- **DI24 = RingLaser_Control_Status** (현장 배선 기준)
- 즉 "Core·Ring 두 레이저의 Control Status가 **모두 ON**" 일 때 `IsLaserReady = true`.
> 참고: 기존 DI9는 **CoreLaser_Laser_Ready** 단독(Core만, Ring 미반영)이었음.
> 또한 `LaserIOControl.GetLaserReady()`는 `CoreLaser_Laser_Ready(9) && RingLaser_Laser_Ready(13)`로 *Laser_Ready* 비트를 봄(이번 변경은 *Control_Status* 비트 기준이라 다름).
---
## 2. enum 불일치 (중요)
`PAIX_PrismaticScanner.ePrismaticDIAddress`에 **`RingLaser_Control_Status = 23`** 으로 되어 있으나,
**현장 실제 DI는 24**. → enum 값이 outdated.
- 영향 파일(둘 다 동일 enum):
- `WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs` (L35)
- `Machines/ILSOS_PrismaticRD_PTOP/Device/IO/PAIX_PrismaticScanner.cs` (L28)
- (참고: `Machines/ILSOS_PrismaticScanner/Device/IO/PAIX_PrismaticScanner.cs` 도 동일)
- 권장: enum을 `RingLaser_Control_Status = 24` 로 정정. (정정 시 enum 상수로 깔끔하게 참조 가능)
---
## 3. 수정 방안 (적용 예정)
### 3.1 `OnMonitoring()` — DI 9 → (DI 12 && DI 24)
```csharp
try
{
// Laser Ready: Core·Ring Control Status 둘 다 ON 일 때만 Ready
// DI 12 = CoreLaser_Control_Status, DI 24 = RingLaser_Control_Status(현장)
if (_IODevice.ReadDI(12) && _IODevice.ReadDI(24))
{
IsLaserReady = true;
}
else
{
IsLaserReady = false;
}
}
catch (Exception ex)
{
Log.E($"[OnMonitoring] DI 읽기 실패: {ex.Message}");
IsLaserReady = false;
}
```
### 3.2 `PerformCmdLaserReadyCheck()` — 변경 없음
- DO 9(CoreLaser_Control)·13(RingLaser_Control) 토글 유지.
### 3.3 변경 요약
| 위치 | 현재 | 변경 후 |
|---|---|---|
| `OnMonitoring()` | `ReadDI(9)` (CoreLaser_Laser_Ready) | `ReadDI(12) && ReadDI(24)` (Core·Ring Control_Status) |
| `PerformCmdLaserReadyCheck()` | `WriteDoBit(9/13)` | 변경 없음 |
---
## 4. 구현 옵션 (가독성)
- **옵션 A (최소 변경):** 리터럴 `ReadDI(12) && ReadDI(24)` + 주석으로 의미 표기. `_IODevice`가 base `IODevice` 타입이라 그대로 사용 가능. (즉시 적용 용이)
- **옵션 B (정공법):** enum `RingLaser_Control_Status = 24` 정정 후, `_IODevice`를 `PAIX_PrismaticScanner`로 캐스팅하여
`ReadDiBit(ePrismaticDIAddress.CoreLaser_Control_Status) && ReadDiBit(ePrismaticDIAddress.RingLaser_Control_Status)` 로 상수 참조.
- 장점: 매직넘버 제거, 의미 명확. 단점: 캐스팅/타입 의존 추가.
- 권장: 우선 옵션 A로 적용하되, **enum 23→24 정정은 별도로 반드시 반영**(다른 코드가 23으로 참조 시 버그 가능성).
---
## 5. 결론 / 적용 체크리스트
- [ ] `OnMonitoring()` 판정: `ReadDI(9)` → `ReadDI(12) && ReadDI(24)` (AND 확정)
- [ ] 버튼은 DO9/13 그대로 유지
- [ ] `PAIX_PrismaticScanner.ePrismaticDIAddress.RingLaser_Control_Status` **23 → 24** 정정(LaserIOControl.cs / 머신 PAIX 파일)
- [ ] 의미: Core Control Status(12) AND Ring Control Status(24) 모두 ON → Laser Ready
> 상태: 분석/수정 방안만 기록. 실제 코드 수정은 미적용.

View File

@ -1,103 +0,0 @@
# IO Interface Map(DI/DO 데이터 맵) 경로 — 소스상 설정 위치 분석
**분석 일시:** 2026-06-17 16:20:10
**대상:** IODevice가 읽는 DI/DO 데이터 맵(CSV) 경로가 소스 어디서 설정되는가
**관련:** `docs/분석/2026/06/17/150031_laser_ready_di12_di24_change.md` (DI12/24 판정 변경)
---
## 0. 질문 (사용자)
- `OnMonitoring`에서 읽는 DI 12 / 24 같은 IO의 **데이터 맵(CSV)** 의 실제 로드 경로가 소스상 어디서 설정되는지.
---
## 1. 결론
- **실제 경로 문자열 값은 소스에 하드코딩돼 있지 않음.**
- 소스에서 확인되는 것은 (1) 경로를 **읽는 메커니즘**, (2) 경로 **속성 정의**, (3) 디바이스 **등록 시 기본값(빈 값)** 까지.
- 실제 경로는 **저장된 디바이스 Config(설정)** 에 들어 있으며, **Setting → Device(IODevice) Config 화면의 `IOInterfaceMapDI` 필드**(또는 디스크의 해당 config 파일)에서 지정됨.
- 영속화/역직렬화는 Aurora 프레임워크 DLL이 처리 → 이 레포 소스에는 경로 문자열 자체가 없음.
---
## 2. 소스 흐름 (확인된 부분)
### ① 디바이스 등록 — 기본 config 주입 (경로 미지정)
`Machines/ILSOS_PrismaticRD_PTOP/EquipmentSetup.cs:153`
```csharp
registry.Register(CommonDeviceIds.IODevice,
() => new PAIX_PrismaticScanner(AuroraEnv, CommonDeviceIds.IODevice, isSimul),
new IOUnitConfig()); // 기본 IOUnitConfig (경로 빈 값)
```
### ② config의 경로 속성 — 기본값 빈 문자열
`AuroraDevices/AuroraIO/IOUnitConfig.cs`
```csharp
internal class IOUnitConnConfig : IConnConfig
{
[InputFilePath]
public string IOInterfaceMapDI { get; set; } = ""; // DI 맵 경로
[InputFilePath]
public string IOInterfaceMapDO { get; set; } = ""; // DO 맵 경로
...
}
```
- `[InputFilePath]` 어트리뷰트 → Setting 화면에서 파일 경로 지정 필드로 노출.
### ③ 활성화 시 그 경로로 CSV 로드
`AuroraDevices/AuroraIO/IODevice.cs`
```csharp
protected override int DoActivate(IDeviceConfig config) {
_connConfig = config.ConnConfig; // L148
...
LoadIOTable(); // → MappingDIO()
}
// MappingDIO():
var m_connConfig = _connConfig as IOUnitConnConfig;
fileName = m_connConfig.IOInterfaceMapDI; // 실제 로드 경로
StreamReader sr = new StreamReader(fileName, ...);
```
- 읽기 진입: `ReadDI(int)` → `_iOUnit.ReadDiBit(index)` (`IODevice.cs:212`).
---
## 3. 데이터 맵(CSV) 실제 위치 (현장/레포)
- 레포 복사본: `Doc/ILSOSConfig/ILSOS_PrismaticScanner/_device/_io/`
- 현장 런타임: `D:\ILSOSConfig\ILSOS_PrismaticScanner\_device\_io\`
- 기본 파일: `DI.csv`, `DO.csv` + 라인/공정별 변형 다수
(`DI_LINE_CC-CAP_ASSY_WELDING.csv`, `DI_PRIBAT_L2.csv`, `DI_SMW.csv`, `DI_SEALPIN.csv`, `DI_PRE_WELDING.csv`, `DI_PILOT_LINE_ANODE_WELDING.csv`, `DI_PILOT LINE CATHODE WELDING.csv` 등)
- 어떤 파일이 로드되는지는 ②의 `IOInterfaceMapDI` 값(=저장된 Config)이 결정.
### 주의 — 기본 `DI.csv`의 12/24/25는 레이저 Control Status가 아님
```
12,12,START SWITCH,START SWITCH
24,24,FAN 8 ALARM,FAN 8 ALARM
25,25,LASER Ready,LASER Ready
```
- 기본 `DI.csv` 기준 12=START SWITCH, 24=FAN 8 ALARM, 25=LASER Ready.
- 코드(`PAIX_PrismaticScanner.ePrismaticDIAddress`)는 12=CoreLaser_Control_Status, 23=RingLaser_Control_Status로 다르게 보고 있음 → **enum과 실제 로드되는 CSV 맵이 불일치할 수 있음.**
---
## 4. 정리표
| 알고 싶은 것 | 소스 확인 | 위치 |
|---|---|---|
| 경로를 읽는 코드 | O | `IODevice.cs` `MappingDIO()` / `ReadDI()` |
| 경로 속성 정의 | O | `IOUnitConfig.cs` `IOInterfaceMapDI` |
| 디바이스 등록(기본값) | O | `EquipmentSetup.cs:153` |
| 실제 경로 문자열 값 | X (소스에 없음) | 저장된 디바이스 Config / Setting UI |
---
## 5. [TODO / 추후 확인할 것]
- [ ] **현장 PC에서 실제 로드되는 DI/DO 맵 파일 확정**: Setting → Device → IODevice Config 의 `IOInterfaceMapDI` / `IOInterfaceMapDO` 값 확인 (또는 `D:\ILSOSConfig\ILSOS_PrismaticScanner\_device\_io\` 에서 실제 사용 파일).
- [ ] **해당 CSV의 DI 12 / 24 줄 의미 확인**: 레이저 Core/Ring Control Status가 맞는지, 아니면 START SWITCH / FAN 8 ALARM 인지.
- 만약 기본 `DI.csv`(12=START SWITCH, 24=FAN 8 ALARM)가 로드 중이면, `ReadDI(12) && ReadDI(24)` 판정이 의도와 다르게 동작 → CSV 맵 또는 인덱스 재확인 필요.
- [ ] **enum vs CSV 정합성**: `PAIX_PrismaticScanner.ePrismaticDIAddress`(12=CoreLaser_Control_Status, 23=RingLaser_Control_Status)와 현장 CSV 맵이 일치하는지 대조. 불일치 시 어느 쪽을 기준으로 맞출지 결정.
- [ ] (참고) `RingLaser_Control_Status` enum 값은 현재 23으로 두기로 함(현장 표기 23, 조건은 24 리터럴 사용). CSV 확인 후 최종 정합성 점검.
> 상태: 소스 추적/위치 분석만. 실제 현장 Config 값 확인은 미완(위 TODO).

View File

@ -1,67 +0,0 @@
# PLC DeviceMap 갱신 (260617 Laser Interface 반영)
**작업 일시:** 2026-06-17 16:53:41
**근거 문서:** `docs/PtoP 유니셀 용접기 PLC 인터페이스_260617/유니셀 PtoP 단동기 Laser Interface_260617.xlsx`
**대상 경로:** `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\External\PLC`
---
## 0. 작업 요약
- 기존 `PLC_DeviceMap_*` 각 bit/word 파일을 `260617_backup` 으로 백업.
- 위 Excel(시트 M70K=Bit, D70K=Word)을 분석해 4개 맵 파일의 **내부 매핑(Comment)만** 갱신. 파일명/주소행 범위 유지.
- 네이밍: 기존 코드 스타일(`PC_*` / `PLC_*`) 유지. 양극(+)은 `_P` 접미사. 음/양극 모두 Excel대로 채움.
## 1. Excel 구조 → 파일 매핑
| Excel 블록 | 방향 | 대상 파일 | 네이밍 |
|---|---|---|---|
| M70K 좌(70000~) | Laser→PLC (PC 출력) | Bit_Output_M | `PC_*` |
| M70K 우(70500~) | PLC→Laser (PC 입력) | Bit_Input_M | `PLC_*` |
| D70K 좌(70000~) | Laser1→PLC (PC 출력) | Word_Output_D | `PC_*` |
| D70K 우(70500~) | PLC→Laser1 (PC 입력) | Word_Input_D | `PLC_*` |
- Excel 메모: "음/양극 레이저 하나로 사용 중이라 음극만 사용 하기로" (70101 셀). → 단, 요청에 따라 양극(+)도 전부 매핑함.
## 2. 생성 결과 (매핑된 주소)
### ① Bit_Output_M (M70000~70143)
- (-) 70000 LIVE, 70001 AUTO_STATUS, 70002 AUTO_COMPLETE, 70003 MANUAL_COMPLETE, 70004 LIGHT_AL, 70005 HEAVY_AL, 70008 PAGE_CHANGE_COMPLETE, 70009 RECIPE_CHANGE_COMPLETE, 70016~70018 WELD READY_ACK/BUSY/COMPLETE
- (+) 70100~70105, 70108, 70116~70118 WELD_01, 70119~70121 WELD_02, 70124 WELD_MONI_COMPLETE, 70125~70128 WELD_CH2(+MoniComplete), 70133~70135 M_POWER, 70137~70139 PARA(READ_ACK/SET_ACK/CHANGE_REQ)
### ② Bit_Input_M (M70500~70643)
- (-) 70500~70509(LIVE,AUTO,AUTO_REQ,MANUAL_REQ,LIGHT_AL,HEAVY_AL,DOOR,EMO,PAGE_CHANGE_REQ,RECIPE_CHANGE_REQ), 70516~70518 WELD REQ/WAIT/COMP_ACK
- (+) 70600~70608, 70610 DISPLACE_USE, 70616~70618 WELD_01, 70619~70621 WELD_02, 70624 WELD_MONI_COMP_ACK, 70625~70628 WELD_CH2(+MoniCompAck), 70630 LASER2_ERROR_RESET, 70632~70635 LASER2_M_POWER, 70637~70639 LASER2_PC_PARA
### ③ Word_Output_D (D70000~70099)
- 70000 PC_ErrorCode, 70001 PC_PageNo, 70002 PC_RecipeNo
### ④ Word_Input_D (D70500~70599)
- 70500 PLCErrorCode, 70501 PLC_PageNo, 70502 PLC_RecipeNo, 70510 PLC_Head_Theta_Position
## 3. 기존 대비 주요 변경 (delta)
- **70502 (Word_Input): PLC_StepNo → PLC_RecipeNo** (의미 변경)
- **70509 (Bit_Input): PLC_RMS_USED → PLC_RECIPE_CHANGE_REQ** (의미 변경)
- 신규: Recipe_CHG(70009/70509), Weld_02·Weld_CH2·M_Power·PC_Para(양극), DisplaceUse(70610), Head_Theta_Position(70510), PC_RecipeNo(70002/70502)
- 신규 스펙에서 빠진 기존 항목: PC_WELD_NG_JUDGE(70020), PC_RMS_PARAMETER_READ_REQUEST_ACK(70037), PLC_ALARM_RESET(70530), PLC_RMS_PARAMETER_READ_REQUEST(70537)
- 기존 파일 버그 수정: Word_Input 70585~70599 가 `M`으로 오기 → `D`로 정정.
## 4. [TODO / 추후 확인할 것] — 코드 정합성 ⚠️
PLC 맵(주소→의미)이 바뀌었으므로, 주소를 enum으로 읽는 코드와 **불일치** 가능성. 반드시 대조 필요:
- [ ] **`Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs`**
- `eWordInput.PLC_STEP_NO = 2` (D70502) → 새 맵은 **RecipeNo**. StepNo를 코드가 D70502로 읽으면 의미 깨짐. (StepNo 주소 재배정 or 코드 수정 필요)
- `eBitInput.PLC_RMS_USED = 0x09` (M70509) → 새 맵은 **RECIPE_CHANGE_REQ**. `SeqStatusMonitor.UpdateIO`가 `PLC_RMS_USED` 읽음 → 충돌.
- `eBitInput.PLC_ALARM_RESET = 0x1E`(M70530), `PLC_RMS_PARAMETER_READ_REQUEST = 0x25`(M70537) → 새 맵엔 없음. 사용처(`SeqStatusMonitor`, RMS) 확인.
- `eBitOutput.PC_WELD_NG_JUDGE = 0x14`(M70020), `PC_RMS_PARAMETER_READ_REQUEST_ACK = 0x25`(M70037) → 새 맵엔 없음.
- [ ] 신규 양극(+)/Laser2 신호(Weld_02, Weld_CH2, M_Power, PC_Para 등) 사용 여부 및 enum 추가 검토.
- [ ] **70628 Excel 라벨 중복**: Excel J열에서 70627·70628 모두 "Weld_CH2Comp_ACK"로 표기됨. 70628은 출력측(70128 Weld_CH2MoniComp) 대칭으로 보아 `PLC_WELD_CH2_MONI_COMP_ACK_P`로 매핑함 → **현장/PLC 측에 실제 의미 확인 필요.**
- [ ] "음극만 사용" 메모 vs 양극 전부 매핑 — 실제 운영에서 양극 사용 여부 확정.
## 5. 백업
- `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\External\PLC\260617_backup\` 에 기존 `PLC_DeviceMap_*` (복사본 포함) 6개 보관.
> 상태: 4개 CSV 갱신 완료(라이브 config). 코드 정합성(4절)은 미확인 — 추후 작업.

View File

@ -1,89 +0,0 @@
# PLC 맵 ↔ 소스 enum 정합성 분석 (Input/Output 검증 + StepNo + 배수)
**분석 일시:** 2026-06-17 17:13:12
**연계:** `docs/분석/2026/06/17/165341_plc_devicemap_update_260617.md` (맵 갱신 작업)
**대상 소스:**
- `WeldingComponents/WeldingCommon/Devices/PLC/PLCController.Address.cs` (base: `eCommonBit/WordInput/Output`)
- `WeldingComponents/WeldingCommon/Devices/PLC/PLCController.Config.cs` (base 로직)
- `Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCController.Address.cs` (machine: `eBit/WordInput/Output`)
- `Machines/ILSOS_PrismaticRD_PTOP/Device/PLC/PLCContoller.Config.cs` (생성자만)
> 상태: **분석/판단만**. 소스·맵 추가 수정은 미적용.
---
## 1. Input/Output 뒤바뀜 여부 → **안 바뀜 (정상)**
판단 기준 = "기존 Input/Output 파일이 담던 방향" 과 "Excel 블록 방향" 대조:
| 구분 | 기존 CSV | Excel 블록 | 방향 | 결론 |
|---|---|---|---|---|
| **Output** | `Bit_Output_M` = M70000~ , `PC_*` | 좌측 70000 = **"Laser→PLC"** | PC가 PLC로 **씀(OUT)** | 70000 = Output ✓ |
| **Input** | `Bit_Input_M` = M70500~ , `PLC_*` | 우측 70500 = **"PLC→Laser"** | PC가 PLC에서 **읽음(IN)** | 70500 = Input ✓ |
- "Laser" = 우리(레이저측) PC. Laser→PLC = 우리가 PLC로 출력 = **Output**. PLC→Laser = 우리가 읽는 입력 = **Input**.
- 이번에 생성한 4파일도 동일 규칙(Output=70000블록 PC_*, Input=70500블록 PLC_*)으로 작성됨 → **방향 정상, 파일명 안 바뀜.**
- Word도 동일: `Word_Output_D`=D70000(PC_*), `Word_Input_D`=D70500(PLC_*). ✓
---
## 2. enum index = (주소 − base) 오프셋 (★ 배수/읽기 핵심)
- CSV 파서(`ParsePLCBitWordAddressFromCSV`)는 행 순서대로 index 0,1,2… 부여. 행이 연속(M70500,70501,…)이므로
**enum 값(offset) = (M주소 − base)**.
- 예: `PLC_WELD_READY_REQ = 0x10`(16) ↔ M70516, `PLC_AUTO_STATUS_P = 0x65`(101) ↔ M70601. ✓
- 즉 소스 enum은 "주소"가 아니라 "오프셋"을 들고 있음. 맵에서 주소 의미가 바뀌면 **같은 오프셋을 읽는 코드가 다른 신호를 읽게 됨.**
### 배수(읽기 블록) 우려 — 확인 필요
- Bit는 word(16bit) 단위로 읽힘 → 대량 read count는 16의 배수.
- 이번 신규 최대 오프셋: **Bit In 139**(M70639 Laser2 PC_Para CHG_ACK), **Bit Out 139**(M70139), **Word In 10**(D70510 Head_Theta), **Word Out 2**(D70002).
- offset 139는 word #8(128~143) 안 → Bit는 **최소 9 word(144bit)** 읽어야 갱신됨. Word In은 **최소 11 word** 읽어야 D70510 갱신.
- [확인필요] PLCBase/통신의 **read count(블록 길이)** 가 위 최대 오프셋을 포함하는지. 작으면 고오프셋(특히 Laser2 130~139, Head_Theta 10)이 **안 읽힘**. (사용자 지적 "배수라 못 읽을 수도")
---
## 3. StepNo "왜?" — 코드가 D70502를 StepNo로 읽음
- 코드: `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPrismaticScanner.cs:422`
`_TotalWeldingStep = ReadWord(eWordInput.PLC_STEP_NO)` → **offset 2 = D70502** 를 "총 용접 step" 으로 사용.
- 기존 `Word_Input` CSV: D70502 = PLC_StepNo (코드와 일치).
- **신규 Excel: D70502 = PLC_RecipeNo** 로 재배정 → 같은 offset 2를 코드는 StepNo로 읽음 → `_TotalWeldingStep` 이 **RecipeNo 값을 읽게 됨(오동작)**.
- 판단: PLC 인터페이스가 StepNo를 빼고 offset 2에 RecipeNo를 넣은 것. **StepNo 신호 위치(또는 폐지 여부)와 RecipeNo offset 확정 필요.** (CSV 코멘트만으론 코드 동작 안 바뀜 — 실제 PLC가 그 주소에 무엇을 싣는지가 핵심)
---
## 4. 신규 맵 ↔ 코드 enum 충돌 (사용처 확인됨)
신규 Excel 맵이 **기존 사용 중인 신호를 빈칸/재배정** 한 것들:
| 신호(offset/주소) | 코드 사용처 | 신규 맵 | 영향 |
|---|---|---|---|
| `PLC_RMS_USED` (0x09 / M70509) | `SeqStatusMonitor.cs:106` `Status.RMSUse` | 70509=RECIPE_CHANGE_REQ | RMSUse가 Recipe변경요청 비트를 읽음 ⚠️ |
| `PLC_ALARM_RESET` (0x1E / M70530) | `SeqStatusMonitor.cs:89` | 70530 빈칸 | 알람리셋 신호 안 들어옴 ⚠️ |
| `PLC_RMS_PARAMETER_READ_REQUEST` (0x25 / M70537) | `PLCController.Config.cs:114`, `RMSHandler.cs:69` | 70537 빈칸 | RMS 파라미터 read req 동작 안 함 ⚠️ |
| `PC_WELD_NG_JUDGE` (0x14 / M70020) | `SeqAutoRunPrismaticScanner.cs:429/896` write | 70020 빈칸 | NG 판정 PLC로 안 나감 ⚠️ |
| `PC_RMS_PARAMETER_READ_REQUEST_ACK` (0x25 / M70037) | `RMSHandler.cs:63/70/76` write | 70037 빈칸 | RMS ACK 안 나감 ⚠️ |
- 신규 추가 신호(코드 enum 없음): Recipe_CHG(70009/70509), DisplaceUse(70610), Weld_02(70619~621), WeldMoni(70624), Weld_CH2(70625~628), Laser2 M_Power/PC_Para(70630~639), Head_Theta(D70510), RecipeNo(D70002/70502).
---
## 5. 정합성 작업 시 방향 (제안 — 미적용)
- enum 2계층 모두 정렬 필요:
1. **base** `eCommonBit/WordInput/Output` (WeldingCommon) — Live/AutoReq/RMS 등 공통 로직.
2. **machine** `eBit/WordInput/Output` (PrismaticRD_PTOP) — StepNo/NG_JUDGE/ALARM_RESET/RMS_USED 등.
- 기존 소스 최대한 유지 + **enum 값(offset)만** 신규 맵에 맞게 재매핑. 단:
- 빠진 신호(RMS, ALARM_RESET, NG_JUDGE)를 신규 맵에 **다시 넣을지 / 코드에서 제거할지** = PLC 인터페이스 측 확정 필요.
- StepNo offset 확정(폐지 or 이동).
- read count(배수)가 신규 최대 offset(139, Word 10) 포함하도록 확인.
- ⚠️ 동일 enum이 4개 머신(PrismaticRD_PTOP / PrismaticScanner / PrismaticCleanLaser / PrismaticGun)에 중복 존재 → PTOP만 바꿀지 공통 정책인지 결정.
---
## 6. [TODO]
- [ ] PLC 측에: StepNo 폐지/이동 여부, 빠진 RMS·ALARM_RESET·NG_JUDGE 주소 확정.
- [ ] read count(블록 길이/배수) 가 offset 139·Word10 포함하는지 검증.
- [ ] 확정 후 base + machine enum offset 재매핑 (기존 코드 유지, 값만).
- [ ] 70502(StepNo→RecipeNo), 70509(RMS_USED→RecipeCHG) 의 코드 영향 반영.

View File

@ -1,48 +0,0 @@
# PLC DeviceMap 갱신 — 추후 확인 체크리스트 (260617)
**작성:** 2026-06-17 17:16:42
**상태:** 분석/맵파일 갱신만 완료. **소스(enum) 수정·코드 반영은 미적용 (보류).**
## 관련 자료 (이미 남긴 분석)
- 맵 갱신 작업·델타: `docs/분석/2026/06/17/165341_plc_devicemap_update_260617.md`
- 방향/StepNo/enum/배수 분석: `docs/분석/2026/06/17/171312_plc_map_io_direction_and_enum_alignment.md`
- 근거 Excel: `docs/PtoP 유니셀 용접기 PLC 인터페이스_260617/유니셀 PtoP 단동기 Laser Interface_260617.xlsx`
- 백업: `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\External\PLC\260617_backup\`
---
## 이미 완료한 것 (참고)
- [x] 기존 `PLC_DeviceMap_*` 6개 → `260617_backup` 백업
- [x] Excel(M70K/D70K) 분석, 4개 CSV 내부 매핑 갱신(파일명·범위 유지, 코드 스타일 네이밍, 음/양극 전부)
- [x] Input/Output 방향 검증 → **안 바뀜(정상)** 확인
- [x] Word_Input 70585~70599 `M`오기 → `D` 정정
---
## 추후 확인 체크리스트 (PLC 인터페이스 측 협의 필요)
### A. 의미 재배정된 주소 (코드 영향 큼)
- [ ] **D70502: StepNo → RecipeNo** — `SeqAutoRunPrismaticScanner.cs:422` 가 `eWordInput.PLC_STEP_NO`(offset 2)로 `_TotalWeldingStep` 읽음. StepNo **폐지? 다른 offset 이동?** 확정. RecipeNo offset 확정.
- [ ] **M70509: PLC_RMS_USED → RECIPE_CHANGE_REQ** — `SeqStatusMonitor.cs:106` `Status.RMSUse` 가 offset 9 읽음. RMS_USED 위치 재확정.
### B. 신규 맵에서 빠진(빈칸) 기존 사용 신호 — 코드 제거 vs 주소 재부여 결정
- [ ] **PLC_ALARM_RESET** (M70530) — `SeqStatusMonitor.cs:89` 사용. 빠지면 알람리셋 동작 안 함.
- [ ] **PLC_RMS_PARAMETER_READ_REQUEST** (M70537) — `PLCController.Config.cs:114`, `RMSHandler.cs:69` 사용.
- [ ] **PC_WELD_NG_JUDGE** (M70020) — `SeqAutoRunPrismaticScanner.cs:429/896` write. 빠지면 NG 판정 PLC 미전달.
- [ ] **PC_RMS_PARAMETER_READ_REQUEST_ACK** (M70037) — `RMSHandler.cs:63/70/76` write.
### C. 신규 추가 신호 — enum 추가/사용 여부 검토
- [ ] Recipe_CHG(70009/70509), DisplaceUse(70610), Weld_02(70619~621), WeldMoni(70624), Weld_CH2(70625~628), Laser2 M_Power/PC_Para(70630~639), Head_Theta_Position(D70510), RecipeNo(D70002/70502)
- [ ] **70628 Excel 라벨 중복**("Weld_CH2Comp_ACK" 70627·70628 동일) — 출력 대칭상 70628=MoniComp_ACK로 매핑함. 실제 의미 확인.
### D. 읽기 블록(배수) 검증
- [ ] PLC **read count(블록 길이)** 가 신규 최대 offset 포함하는지 — Bit In/Out **offset 139**(M70639/M70139), **Word In offset 10**(D70510). Bit는 16비트(word) 배수로 읽힘 → 최소 9 word(144bit) / Word In 11 word 필요. 작으면 Laser2(130~139)·Head_Theta 미갱신.
### E. enum 정렬 범위/정책
- [ ] **enum 2계층** 모두 정렬: base `eCommonBit/WordInput/Output`(WeldingCommon) + machine `eBit/WordInput/Output`(PrismaticRD_PTOP). 기존 소스 유지, **offset 값만** 재매핑.
- [ ] 동일 enum이 **4개 머신**(PrismaticRD_PTOP / PrismaticScanner / PrismaticCleanLaser / PrismaticGun)에 중복 — PTOP만 변경할지, 공통 적용할지 정책 결정.
---
## 결론
- 맵 CSV 갱신은 반영됨. **소스 enum/코드는 위 A~E 확정 후** 수정 예정. 지금은 보류.

View File

@ -1,264 +0,0 @@
# 현장 조명(EROOM) PC 제어 — 소스 구조 분석 및 수정 가이드
**분석 일시:** 2026-06-17 18:40:11
**요청자:** 안동준 과장님 요청사항 — *"조명을 PC에서 조정할 수 있도록"*
**대상 조명:** 현장 EROOM 조명 (제조사: **이룸인더스트리**, eroomi.com)
**연결 상태:** PC ↔ 조명 컨트롤러 **RS232 9핀** 직결
**목적:** 현재 조명을 무엇이 제어하는지 파악하고, 나중에 PC 제어로 수정하기 위한 기준 문서
---
## 0. 요청 내용 (사용자)
- 현장 조명에 **MANUAL / REMOTE** 두 모드가 있고, **REMOTE**로 두면 PC와 통신 가능할 텐데 현재는 **소스 내에서 연결이 안 된 상태**.
- 조명을 조정하는 게 무엇인지 분석해서 docs에 정리(나중에 수정 예정).
- 조명은 **EROOM 조명**이며 PC에 **RS232 9핀 포트로 연결**되어 있음. → *장치관리자에서 포트 확인해야 하나?*
- Device에 **`Light`** 라는 디바이스로 등록되어 있을 것.
---
## 1. 결론 요약
1. 소스에는 이미 **`Light` 디바이스**가 등록되어 있다.
- 위치: `WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs:226`
- 등록: `CommonDeviceIds.Light` → `new LightDevice(..., eLight.ERI, isSimul)`
- 즉 **드라이버 타입이 `eLight.ERI`(ERI 4채널)** 로 고정되어 있다.
2. 조명 통신은 **RS232 시리얼** 기반이다. (`LightConfig` 의 `SerialConnConfig`)
- 기본값: **PortNum=1, BaudRate=19200, 8-N-1, ReadTimeout=500**
3. **"연결 안 됨"의 유력한 원인 3가지**:
- (a) **드라이버 불일치**: 현재 `eLight.ERI` 드라이버는 ERI사 ASCII 프로토콜(`setbrightness ch val\r` 등)을 쓴다. EROOM 컨트롤러의 프로토콜과 **명령어 포맷이 다르면** 같은 포트에 연결돼도 동작하지 않는다.
- (b) **시리얼 설정 불일치**: EROOM(EUV 계열) 컨트롤러는 **38,400bps** 사양이 확인된다. 현재 소스 기본값은 **19200**. → BaudRate/포트번호 불일치 가능.
- (c) **컨트롤러 모드**: 컨트롤러 전면 스위치가 **MANUAL**이면 RS232 명령을 무시한다. → 반드시 **REMOTE**로 둬야 PC 명령이 먹는다.
4. 따라서 PC 제어를 살리려면 **① 컨트롤러 REMOTE 전환 → ② COM 포트/Baud 맞추기 → ③ (필요 시) EROOM 전용 드라이버 추가** 순서로 접근한다.
---
## 2. 장치관리자 COM 포트 확인 (사용자 질문 답변)
**네, 먼저 장치관리자에서 포트를 확인하는 게 맞습니다.**
- RS232 9핀이 메인보드 시리얼이든 USB-to-Serial 젠더든, Windows에서는 **`포트(COM & LPT)`** 항목에 **`COMx`** 로 잡힙니다.
- 확인 순서:
1. `장치관리자` → `포트(COM & LPT)` 펼치기 → 조명 케이블 연결/분리 시 나타났다 사라지는 항목이 해당 포트.
2. 그 항목 더블클릭 → `포트 설정` 탭에서 **비트/초(Baud), 데이터 비트, 패리티, 정지 비트**가 컨트롤러 사양과 일치하는지 확인.
3. 잡힌 **COM 번호**를 소스의 `LightConfig.ConnConfig.PortNum` 과 맞춰야 함.
- USB-Serial인 경우 칩셋 드라이버(예: FTDI, Prolific, CH340)가 설치돼 있어야 COM 포트가 잡힙니다. 안 잡히면 드라이버부터 설치.
- 포트가 다른 프로그램(터미널 등)에서 점유 중이면 프로그램에서 열 수 없으니, 점검용 터미널은 닫고 테스트.
> 점검 팁: 장치관리자에서 COM 번호/Baud를 확인한 뒤, 시리얼 터미널(예: 테라텀)로 컨트롤러를 **REMOTE 모드**에 두고 EROOM 매뉴얼 명령을 직접 쳐서 조명이 반응하는지부터 확인하면, 배선/포트 문제인지 소스 문제인지 분리할 수 있다.
---
## 3. 소스 구조 — 조명을 제어하는 경로
### 3.1 디바이스 등록 (시작점)
`WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs:226`
```csharp
registry.Register(CommonDeviceIds.Light,
() => new LightDevice(CommonDeviceIds.Light, eLight.ERI, isSimul), // ← 드라이버 타입 ERI 고정
new LightConfig(),
device => new UCLight() { DataContext = new UCLightViewModel(AuroraEnv, CommonDeviceIds.Light, _SequenceSetting.LightValue) });
```
### 3.2 디바이스 본체 / 드라이버 선택
`AuroraDevices/AuroraLight/LightDevice.cs`
- `eLight` enum: **`LvsPn`, `ALT_4CH`, `ALT_8CH`, `ERI`** — **EROOM 없음.**
- `DoActivate()` 에서 타입에 따라 실제 드라이버 인스턴스 생성:
- `eLight.ERI` → `new ERILight(DeviceId, IsSimul, 4)` (4채널)
- 연결: `_light.InitInternal(config.ConnConfig as SerialConnConfig)`
### 3.3 시리얼 설정
`AuroraDevices/AuroraLight/LightConfig.cs`
```csharp
ConnConfig = new SerialConnConfig() {
PortNum = 1, // ← 장치관리자 COM 번호와 맞춰야 함
BaudRate = 19200, // ← EROOM(EUV 계열)은 38400 사양 확인됨, 불일치 가능
Parity = Parity.None, StopBits = StopBits.One, DataBits = 8,
ReadTimeout = 500
};
```
### 3.4 ERI 드라이버 프로토콜 (현재 연결되는 명령어)
`AuroraDevices/AuroraLight/ERI/ERILight.cs` — **ASCII 텍스트 + `\r` 종결**
| 동작 | 명령 포맷 |
|---|---|
| 밝기 설정 | `setbrightness {channel} {value}\r` (value 0~255) |
| On/Off | `seton {channel} {0|1}\r` |
| On 상태 조회 | `geton {channel}\r` |
> EROOM 컨트롤러 매뉴얼의 명령어가 위와 다르면, ERI 드라이버로는 통신이 안 된다(=현재 "연결 안 됨" 상태의 핵심 원인 후보).
### 3.5 시퀀스에서의 사용 (자동운전)
`Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPrismaticScanner.cs`
- 변수: `private LightDevice _Light;` (L138)
- 획득: `_Light = context.GetLight();` (L1132) — `SequenceContextEx.GetLight()` → `context.Devices[CommonDeviceIds.Light]`
- 점등: 자동운전 시 `_Light.SetLightValue(lightValues)` 로 4채널 ON (L356~359, L535~537)
- 기본 밝기값: `lightValues = {255,255,255,255}` (L193), 레시피값은 `SequenceSetting.LightValue` / `CompensationLightValue`
---
## 4. EROOM 조명 컨트롤러 (웹 조사 결과)
- **제조사:** 이룸인더스트리 (eroomi.com) — 머신비전 조명/컨트롤러 전문. (사이트 TLS 인증서 문제로 직접 페이지 파싱은 실패, 검색 요약 기준)
- **대표 컨트롤러 사양(EUV-FX7 등 기준):**
- 통신: **RS232, 38,400 bps**, MANUAL/REMOTE 지원
- 출력: **0~100%** 파워 제어
- 채널: **최대 4채널** 점등 start/stop 입력
- 조사 시간(irradiation time) 설정, 전면 엔코더로 파라미터 조정 및 채널값 메모리 저장
- **주의:** 정확한 RS232 **명령어 포맷/체크섬/종결문자**는 제품 매뉴얼로 확정 필요. (밝기 스케일이 ERI의 0~255가 아니라 0~100%일 가능성 높음 → 변환 필요)
---
## 5. 별도로 존재하는 CCS 조명 코드 (참고 — 본 건과 무관)
- `AuroraVision/VisionLight/Light_CCS.cs` + `LightControlCCS_Library.dll`(C++) 가 별도로 존재.
- 단, 이건 **Socket(TCP/IP) 기반**(`managementMain.Connect(ip, port)`)이라 **RS232 EROOM 건과는 연결 방식이 다름**.
- PTOP 빌드 산출물 폴더에 `LightControlCCS_Library.dll` 이 복사돼 있으나, PTOP 소스에서 `Light_CCS` 를 직접 참조하지는 않음. → **이번 건과 혼동 주의.**
---
## 6. 나중에 수정할 때 체크리스트 (코드 수정은 보류, 방안만 정리)
### Step 1 — 하드웨어/포트 확정 (코드 수정 전)
- [ ] 컨트롤러 전면 스위치 **REMOTE** 전환
- [ ] 장치관리자에서 **COM 번호** 확인, **Baud(38400 추정)/패리티/스톱비트** 확인
- [ ] 시리얼 터미널로 EROOM 매뉴얼 명령 직접 전송 → 조명 반응 확인 (배선/포트 문제 격리)
### Step 2 — 설정만으로 해결되는 경우 (EROOM ↔ ERI 프로토콜이 호환될 때)
- [ ] `LightConfig` 의 `PortNum` 을 실제 COM 번호로
- [ ] `BaudRate` 19200 → 38400 등 사양에 맞게
- [ ] (설정값은 가능하면 장비 Setting/UI에서 변경하도록, 하드코딩 지양)
### Step 3 — 프로토콜이 다른 경우 (EROOM 전용 드라이버 신규 추가)
- [ ] `AuroraDevices/AuroraLight/eLight` enum 에 `EROOM` 추가
- [ ] `AuroraDevices/AuroraLight/EROOM/EROOMLight.cs` 신규 (`SerialConn, IEROOMLight` 구현, ERI 클래스 패턴 참고)
- EROOM 매뉴얼대로 명령 포맷/밝기 스케일(0~100%) 구현
- [ ] `LightDevice.DoActivate()` 의 `switch(_lightType)` 에 `case eLight.EROOM` 분기 추가
- [ ] `CommonEquipmentSetup.cs:226` 의 `eLight.ERI` → `eLight.EROOM` 으로 변경
- [ ] `ILight.cs` 에 필요 시 `IEROOMLight` 인터페이스 추가
### Step 4 — 검증
- [ ] 시뮬레이션 해제 후 자동운전에서 `_Light.SetLightValue` 로 점등 확인
- [ ] 수동/UI(`UCLight`)에서 채널별 밝기 제어 확인
---
## 7. 핵심 파일 위치 정리
| 역할 | 경로 |
|---|---|
| 조명 디바이스 등록 | `WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs:226` |
| 조명 디바이스 본체/드라이버 선택 | `AuroraDevices/AuroraLight/LightDevice.cs` |
| 조명 드라이버 인터페이스 | `AuroraDevices/AuroraLight/ILight.cs` |
| 시리얼/포트 설정 | `AuroraDevices/AuroraLight/LightConfig.cs` |
| 현재 사용 중인 ERI 드라이버 | `AuroraDevices/AuroraLight/ERI/ERILight.cs` |
| 시퀀스 사용처 | `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPrismaticScanner.cs` (L138, 1132, 356~, 535~) |
| 디바이스 접근 헬퍼 | `WeldingComponents/WeldingCommon/Sequences/SequenceContextEx.cs:54` (`GetLight`) |
| (참고) CCS Socket 조명 | `AuroraVision/VisionLight/Light_CCS.cs` |
---
---
## 8. eLight.ERI 제어 소스 — 참고 코드/위치 모음
> 현재 `Light` 디바이스가 **`eLight.ERI` 드라이버로 등록**되어 동작하므로, EROOM 제어 구현 시 **그대로 베껴 쓸 수 있는 레퍼런스**가 ERI 계열이다. 아래는 제어 전체 흐름(등록 → 디바이스 → 드라이버 → UI → 시퀀스)의 실제 코드/위치.
### 8.1 전체 제어 흐름 한눈에
```
CommonEquipmentSetup.RegisterDevices() ← 디바이스 등록 (eLight.ERI 지정)
│
▼
LightDevice (AuroraLight) ← 추상 디바이스, DoActivate에서 드라이버 선택
│ _light = new ERILight(...)
▼
ERILight : SerialConn, IERILight ← RS232 ASCII 프로토콜 실제 송수신
▲ ▲
│ │
UCLightViewModel (수동 UI) SeqAutoRun...(자동운전) ← 두 곳에서 제어 호출
```
### 8.2 ① 디바이스 등록 — `WeldingComponents/WeldingCommon/CommonEquipmentSetup.cs`
- L48: 디바이스 활성상태 표시 등록 `.AddDeviceActiveStatus(CommonDeviceIds.Light)`
- L226~227: 디바이스 + UI 등록
```csharp
registry.Register(CommonDeviceIds.Light,
() => new LightDevice(CommonDeviceIds.Light, eLight.ERI, isSimul), // 드라이버 타입
new LightConfig(), // 시리얼 설정
device => new UCLight() { // 제어 UI
DataContext = new UCLightViewModel(AuroraEnv, CommonDeviceIds.Light, _SequenceSetting.LightValue)
});
```
### 8.3 ② 디바이스 본체 — `AuroraDevices/AuroraLight/LightDevice.cs`
- `eLight` enum (L16~22): `LvsPn, ALT_4CH, ALT_8CH, ERI` → **EROOM 추가 지점**
- `DoActivate()` (L40~64): `switch(_lightType)` 에서 드라이버 생성 + `InitInternal(SerialConnConfig)` → **EROOM case 추가 지점**
- 외부 공개 제어 API:
- `SetLightValue(List<int> values)` (L88) / `SetLightValue(int ch, int val)` (L93)
- `CheckLightOnAndValue(int val)` (L100) — ERI 전용으로 채널별 On+밝기 검증
- `DoDeactivate()` (L66~79): ERI는 전 채널 0으로 끄고 시리얼 Close
### 8.4 ③ ERI 시리얼 드라이버 — `AuroraDevices/AuroraLight/ERI/ERILight.cs` (RS232 프로토콜 본체)
| 기능 | 메서드 | 명령/응답 포맷 | 라인 |
|---|---|---|---|
| 연결 | `InitInternal(SerialConnConfig)` | `Open(connConfig)` | L30 |
| 밝기 설정 | `SetLightBrightness(ch, val)` | TX `setbrightness {ch} {val}\r` (val 0~255) | L134 |
| On/Off | `SetLightPowerStatus(ch, on)` | TX `seton {ch} {0\|1}\r` | L122 |
| On 조회 | `GetLightOn(ch, out isOn)` | TX `geton {ch}\r` / RX `geton {idx} {ch} {0\|1}` | L186 |
| 밝기 조회 | `GetLightValue(ch, out val)` | TX `getbrightness {ch}\r` / RX `getbrightness {idx} {ch} {val}` | L219 |
| 채널 검증 | `ChannelValidCheck` (0~ch-1) / `ValueValidCheck` (0~255) | — | L162 / L172 |
- 부모 `SerialConn` 의 `Open / Write / WriteAndReadLine / IsConnected / Close` 사용.
- **EROOM 적용 시 주의:** ASCII 명령어/종결문자(`\r`)/밝기 스케일(0~255)이 EROOM 매뉴얼과 다르면 이 클래스를 그대로 못 쓰고, 동일 구조로 `EROOMLight.cs` 신규 작성.
### 8.5 ④ 수동 제어 UI — `AuroraDevices/AuroraLight/View/UCLightViewModel.cs` (+ `UCLight.xaml`)
- 생성자(L67): `aurora.DeviceManager[deviceId] as LightDevice` 로 디바이스 획득
- ERI일 때 채널/슬라이더 세팅(L76~84): `nChannel=4, nSliderMax=255`
- 슬라이더 → 밝기: `nSliderValue` setter(L21~24) → `_light.SetLightValueInternal(cbChannel, value)`
- 버튼 커맨드:
- `CMD_LightOnOff` → `LightOnOff()` (L111): 채널 토글 On/Off
- `CMD_ALLLightOn` → `ALLLightOn()` (L130): 전 채널 ON
- `CMD_ALLLightOff` → `ALLLightOff()` (L140): 전 채널 OFF
- **참고:** 채널수/스케일이 코드에 하드코딩(`nChannel=4`, `nSliderMax=255`)되어 있음 → EROOM(0~100%) 적용 시 여기 함께 손봐야 함.
### 8.6 ⑤ 자동운전 사용처 — `Machines/ILSOS_PrismaticRD_PTOP/Sequences/SeqAutoRunPrismaticScanner.cs`
- L138: `private LightDevice _Light;`
- L193: `private List<int> lightValues = new List<int> { 255, 255, 255, 255 };`
- L1132: `_Light = context.GetLight();`
- L353~359 / L532~537: 자동운전 진입 시 점등
```csharp
if (_SequenceSetting.UseCellOffset) UpdateAutoLightValue(_SequenceSetting.CompensationLightValue);
else UpdateAutoLightValue(_SequenceSetting.LightValue);
if (!_Light.CheckLightOnAndValue(lightValues[0])) { // 현재 밝기와 다르면
_Light.SetLightValue(lightValues); // 4채널 점등
Log.I($"Light All On !");
}
```
- L1357: `UpdateAutoLightValue(int value)` — `lightValues` 갱신 헬퍼
### 8.7 ⑥ 디바이스 접근 헬퍼 — `WeldingComponents/WeldingCommon/Sequences/SequenceContextEx.cs`
```csharp
public static LightDevice GetLight(this SequenceContext context) // L54
=> context.Devices[CommonDeviceIds.Light] as LightDevice; // L55
```
### 8.8 인터페이스 / 설정 정의
- `AuroraDevices/AuroraLight/ILight.cs` — `ILight` 공통 인터페이스 + `IERILight`(L30, `IsOn`/`GetLightOn`/`GetLightValue` 추가). → EROOM용 `IEROOMLight` 추가 지점.
- `AuroraDevices/AuroraLight/LightConfig.cs` — `SerialConnConfig`(PortNum/BaudRate/Parity/StopBits/DataBits/ReadTimeout) 정의.
---
> ⚠️ 본 문서는 **분석/가이드 전용**이며 코드는 수정하지 않았다. 실제 수정 전 EROOM 컨트롤러 매뉴얼(RS232 명령어/Baud/밝기 스케일)로 사양을 확정할 것.

View File

@ -1,108 +0,0 @@
# Manual Laser Power Control GroupBox 2개 분석 (Percent vs Watt)
- **작성일:** 2026-06-18
- **대상 파일:**
- View: `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml`
- ViewModel: `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs`
---
## 0. 내 요청사항 (배경)
> Laser Voltage를 1V 주면 몇 W 나오는지 측정하는 **캘리브레이션(Cal) 파일**을 만들고 싶다.
> 이때 **레이저는 실제로 발진시키지 말고**, **아날로그 신호만 살려서** RTC5의 기록은 그대로 저장되도록 할 수 있는가?
>
> - 메뉴얼 화면 → Manual Laser Power Control 영역에서 레이저를 쏠지/안 쏠지 제어.
> - 보통 **레이저 컨트롤러 IO를 안 살리면** 레이저가 실제로 안 나간다.
> - 아날로그 신호만 활성화해서, 레이저는 안 나가되 RTC5 출력 기록은 정상 저장 가능한지 검토.
→ 이 분석은 위 요청과 직접 연관됨. 특히 **아날로그 출력(WriteDA)** 과 **레이저 발진 신호(LaserSignalOn)** 가 코드상 분리되어 있다는 점이 핵심.
---
## 1. 결론 요약
- XAML에 `Header="Manual Laser Power Control"` GroupBox가 **2개**(Grid.Row="0", Grid.Row="1") 있고, **레이아웃·구성 컨트롤은 100% 동일**하다.
- **유일한 실질 차이는 "파워 입력 단위"** 다.
- **Row 0 = Percent(%) 기준** → `LaserPower1Percent`/`LaserPower2Percent`, 버튼 → `CMD_LaserShotOnPercent`
- **Row 1 = Watt(W) 기준** → `LaserPower1Watt`/`LaserPower2Watt`, 버튼 → `CMD_LaserShotOnWatt`
- 즉 **똑같은 UI를 % 입력용 / W 입력용 두 벌로 둔 것**. Frequency·Pulse Width·Timer·Laser Off·Result 표시는 둘이 **완전히 공유**(같은 바인딩/커맨드).
---
## 2. 두 GroupBox 차이 상세
| 항목 | Row 0 GroupBox (Percent) | Row 1 GroupBox (Watt) |
|---|---|---|
| Power #1 바인딩 | `LaserPower1Percent` (Unit `%`) | `LaserPower1Watt` (Unit `W`) |
| Power #2 바인딩 | `LaserPower2Percent` (Unit `%`) | `LaserPower2Watt` (Unit `W`) |
| Frequency | `LaserPeriod` (kHz) **공유** | `LaserPeriod` (kHz) **공유** |
| Pulse Width | `LaserPulseWidth` (us) **공유** | `LaserPulseWidth` (us) **공유** |
| Timer Set (Spot Time) | `IsUseLaserAutoOffChecked`, `LaserOnTime` **공유** | 동일 **공유** |
| Laser Shot Result | `BusyStatus`, `ListInterationCount` **공유** | 동일 **공유** |
| **Laser On 버튼** | `CMD_LaserShotOnPercent` | `CMD_LaserShotOnWatt` |
| **Laser Off 버튼** | `CMD_LaserShotOff` **공유(동일)** | `CMD_LaserShotOff` **공유(동일)** |
> 정리: **다른 것은 "Power 입력 단위(% vs W)와 그에 대응하는 Laser On 커맨드" 뿐**. 나머지는 같은 ViewModel 프로퍼티/커맨드를 그대로 쓴다.
---
## 3. 각 동작이 어디에 영향을 끼치는가 (ViewModel 분석)
### 3-1. Laser On — `PerformCmdLaserShotOnPercent` / `PerformCmdLaserShotOnWatt`
두 메서드는 구조가 동일하며, **`IsUseLaserAutoOffChecked`(Use Spot Time 토글) 값에 따라 두 갈래**로 갈린다.
#### (A) `IsUseLaserAutoOffChecked == true` (= "Use Spot Time")
- 시퀀스(`SeqTeachingRun`)를 통해 발사. `CreateDotForLaserOn()`으로 현재 위치에 Dot 1개를 만들고 시퀀스 Start.
- **주의:** 이 분기는 Percent/Watt 둘 다 `CreateDotForLaserOn()`을 호출하는데, 이 함수 내부는 **`LaserPower1Percent`/`LaserPower2Percent`(% 기준)만 사용**한다(`LaserPowerCalParam.MaxCenterPower/MaxRingPower` 비율 환산). 즉 **Spot Time 모드에서는 Watt 버튼을 눌러도 실제로는 Percent 값으로 동작**한다. (Watt 입력이 무시되는 잠재 이슈)
- 사전 가드: Scanner Busy 체크 → `_Laser.GetLaserReady()` 체크 → 시퀀스 Running 체크 → 모델 선택 체크 → Blow 설정 반영.
#### (B) `IsUseLaserAutoOffChecked == false` (= "No Use Spot Time") ★ Cal 작업 관련 핵심
직접 아날로그/펄스 제어 후 레이저 신호 ON:
```
powerCalParam.Initialize(...);
// 단위 환산만 다름:
// Percent 버튼: CalibratePercentToMeasuredVoltage(LaserPowerNPercent) * 10
// Watt 버튼: CalibrateWattToMeasuredVoltage(LaserPowerNWatt) * 10
Thread.Sleep(50);
_Scanner.WriteDA(DA1, powerVolt1, COMMAND); // ← 아날로그 전압 출력 (DA1)
_Scanner.WriteDA(DA2, powerVolt2, COMMAND); // ← 아날로그 전압 출력 (DA2)
_Scanner.SetLaserPulseCtrl(LaserPeriod*1000, LaserPulseWidth); // 펄스 주파수/폭
_Scanner.LaserSignalOn(); // ← 레이저 발진 게이트 ON (실제 발사 트리거)
```
- **TruFiberType 분기:** `TruFiberS`는 DA1만(단일빔), `TruFiberP` 등은 DA1=Center / DA2=Ring 두 빔으로 환산.
- **`* 10`**: 환산된 전압(0~1V 등 정규화 값)을 RTC5 DA 풀스케일(10V 기준)로 스케일링하는 것으로 보임.
> **요청사항과의 연결:**
> - **아날로그 출력(`WriteDA`)** 과 **레이저 발진(`LaserSignalOn`)** 이 **코드상 별개 호출**이다.
> - 즉, 구조적으로 "아날로그 전압은 DA 포트로 내보내되 `LaserSignalOn()`은 호출하지 않거나, 레이저 컨트롤러 IO를 비활성"으로 두면 → **DA 아날로그 값은 출력되지만 실제 발진은 안 되는** 형태가 성립할 여지가 있다.
> - 단, "RTC5 기록이 그대로 저장되는가"는 별도 확인 필요(아래 §5 미해결 질문).
### 3-2. Laser Off — `PerformCmdLaserShotOff` (두 GroupBox 공통)
- `IsUseLaserAutoOffChecked == true` → `_Scanner.StopExecution()` (시퀀스/리스트 중단)
- `false` → `_Scanner.LaserSignalOff()` (발진 게이트 OFF)
- 공통으로 `_Scanner.SetStandby(10, 0)`.
### 3-3. Monitoring — `OnMonitoring` (100ms DispatcherTimer)
- Scanner 활성 체크, Encoder X/Y/속도 갱신, `GetMarkingInfo()`로 FieldOut X/Y 판정, `ReadStatus()`로 List1/List2 Busy 및 반복 횟수(`ListInterationCount`) 갱신.
- View가 Visible일 때만 동작(`CMD_VisibleChanged`).
### 3-4. Marking On The Fly (Row 2, 별개 GroupBox)
- Encoder 기반 OTF 마킹 영역. `CMD_ActivateOnTheFly`/`Deactivate`, `ResetEncoder`, `SimulateEncoder(X/XY)`.
- 본 분석의 핵심(Manual Laser Power Control 2개)과는 별개 기능.
---
## 4. "똑같은가?" 에 대한 답
- **UI 레이아웃/구성: 똑같다.** (복붙 수준으로 동일)
- **기능: 다르다.** 한쪽은 **% 입력 → Percent 캘리브레이션**, 다른 한쪽은 **W 입력 → Watt 캘리브레이션**으로 전압을 만든다.
- **공유 리스크:** "Use Spot Time" 분기에서는 Watt 버튼도 내부적으로 Percent 값으로 동작(`CreateDotForLaserOn`이 Percent 전용) → Watt 의도와 불일치 가능. Cal 측정은 보통 "No Use Spot Time"(직접 DA 제어) 분기를 쓰므로 이 리스크와는 분리됨.
---
## 5. 미해결 / 추가 확인 필요 (Cal 파일 작업 관련)
1. **`WriteDA`만으로 RTC5가 출력을 "기록/저장"하는가?** — 발진 없이 DA만 출력했을 때 RTC5 로깅 동작 확인 필요.
2. **`LaserSignalOn()` 생략 또는 레이저 컨트롤러 IO 비활성** 조합이 실제 장비에서 "아날로그만 살아있고 발진 안 됨"으로 동작하는지 현장 검증 필요.
3. **`* 10` 스케일과 `CalibrateWattToMeasuredVoltage` / `CalibratePercentToMeasuredVoltage`** 의 입출력 단위(반환값이 V 인지, 0~1 정규화인지) 정확한 정의 확인 → 1V당 W 환산표 작성의 기준.
4. Cal 측정 시 권장 경로: **"No Use Spot Time"(IsUseLaserAutoOffChecked=false) + Watt(또는 직접 전압) 모드**로 DA 출력값을 단계적으로 인가하며 파워미터 측정 → CSV(`PowerCalibrationCenterBeamCSVFile` / `RingBeamCSVFile`) 작성.

View File

@ -1,81 +0,0 @@
# PTOP 가 비-PTOP(Scanner/TDC/Gun/CleanLaser) 경로에서 참조/가져오는 파일 분석
- **작성일:** 2026-06-18
- **대상:** `Machines/ILSOS_PrismaticRD_PTOP`
- **목적:** PTOP 경로가 아닌 Scanner/TDC/Gun/CleanLaser 경로에서 참조해 가져오는 파일을 식별하고, PTOP 경로로 복사할 대상을 정리. (소스 무수정 원칙)
---
## 0. 내 요청사항
> 참조해서 가져오는 파일 중에 PTOP 경로가 아닌 **SCANNER, TDC, GUN, CleanLaser** 경로에서 가져오는 파일들을 **PTOP 경로에 복사**해줘. 분석내용도 늘 정리해주고 **소스는 절대 건들지 마**.
---
## 1. 결론 요약 (먼저)
- **소스/프로젝트 레벨에서는 다른 머신 폴더의 파일을 가져오지 않는다.**
- PTOP `.csproj`의 `Compile/Content/Page/Resource Include`는 **전부 자기 폴더 내 상대경로**.
- `ProjectReference`는 **공유 프로젝트(AuroraDevices, AuroraVision, WeldingComponents)뿐** — Scanner/Gun/CleanLaser/TDC 프로젝트를 참조하지 않음.
- 소스/XAML 문자열에 다른 머신 폴더(`ILSOS_PrismaticScanner\` 등)를 가리키는 **파일 경로 참조 없음**(컴파일된 `.dll` 바이너리 매칭만 존재 → 무의미).
- 즉 **PTOP는 Scanner 프로젝트의 자체 포크**다. `EquipmentSetup.cs`가 `namespace ILSOS_PrismaticScanner`를 그대로 사용하고, `using ILSOS_PrismaticScanner.*`도 자기 자신(같은 네임스페이스) 또는 WeldingCommon으로 해석됨.
- **유일한 비-PTOP 경로 참조는 "빌드 출력/실행 경로"가 TDC를 가리키는 것**이다. (아래 §2)
- 따라서 **git 트래킹된 소스 중 복사할 "참조 파일"은 없다.** 실제로 다른 머신 경로에서 가져오는 것은 **런타임 출력(_out) 폴더 안의 빌드 산출물**이며, 이 폴더들은 **git 비추적(.gitignore)** 이다. (아래 §3)
---
## 2. 빌드/실행 경로의 TDC 참조 (PTOP.csproj)
| 항목 | 값 | 의미 |
|---|---|---|
| `OutputPath` (Debug\|x64) | `..\_out_prismatic_rd_tdc\x64-Debug\` | PTOP DLL이 **TDC 출력 폴더**로 빌드됨 |
| `OutputPath` (Release\|x64) | `..\_out_prismatic_rd_tdc\x64-Release\` | 동일 |
| `StartProgram` | `$(SolutionDir)\Machines\_out_prismatic_rd_tdc\x64-Debug\Aurora.exe` | 실행 시 **TDC 폴더의 Aurora.exe** 기동 |
→ PTOP는 빌드/실행을 **`_out_prismatic_rd_tdc`**(TDC 경로)에서 수행한다. 별도로 존재하는 `_out_prismatic_rd_ptop` 폴더는 현재 빌드 구성에서 **사용되지 않음**.
> 참고: `EquipmentSetup.cs` 주석에 `// 변경 TDC->PTOP`, `register(... SeqAutoRunTDC)` 등 TDC→PTOP 이행 흔적이 다수 있음. 즉 TDC 산출물 위에서 PTOP가 동작하도록 과도기 상태.
---
## 3. 런타임 출력 폴더 구조 (실제 "가져오는" 파일의 위치)
`Machines/_out_prismatic_rd_tdc/x64-Debug/` (PTOP가 실제 구동되는 폴더) 내부:
- 공유 데이터/설정: `_alarm`, `_config_ingredient`, `_config_model`, `_config_recipe`, `_config_setting`, `_process`, `_vision`, `Light`, `RTC5_2024_09_27`, `AccountStorage` 등
- **머신별 프로파일 서브폴더** (각 폴더에 `Aurora.exe.config`, `hislog.config` 2개씩):
- `ILSOS_PrismaticScanner/`
- `ILSOS_PrismaticGun/`
- `ILSOS_PrismaticCleanLaser/`
- `ILSOS_PrismaticRD_TDC/`
- **→ `ILSOS_PrismaticRD_PTOP/` 폴더는 없음** (PTOP 전용 프로파일 폴더가 부재)
**git 추적 상태:** `_out_prismatic_*` 폴더는 **추적 파일 0개** (전부 .gitignore 빌드 산출물).
---
## 4. "복사 대상" 판정
- **소스 레벨 복사 대상: 없음.** (다른 머신 폴더의 소스를 참조하지 않음 → 복사할 것이 없음. 소스 무수정 원칙과도 부합.)
- **런타임 레벨 후보:** 머신별 프로파일 폴더(`Aurora.exe.config`, `hislog.config`)가 Scanner/TDC/Gun/CleanLaser용으로는 있는데 **PTOP용(`ILSOS_PrismaticRD_PTOP/`)이 없음.**
- 가장 그럴듯한 작업: TDC(또는 Scanner) 프로파일을 **`ILSOS_PrismaticRD_PTOP/` 폴더로 복사**해 PTOP 전용 프로파일을 만드는 것.
- 단, 이 폴더는 **빌드 시 재생성되는 비추적 산출물**이라 다음 빌드에서 덮어쓰일 수 있음(영속성 한계).
---
## 5. 미해결 / 확인 필요
1. **사용자가 의도한 "참조 파일"이 §3의 머신별 프로파일(config) 폴더가 맞는지** 확인 필요. (소스가 아니라 런타임 config로 보임)
2. **목적지 "PTOP 경로"** 가 (a) 런타임 `_out_prismatic_rd_tdc/.../ILSOS_PrismaticRD_PTOP/` 인지, (b) `_out_prismatic_rd_ptop/` 인지, (c) 소스 트리의 PTOP 프로젝트 폴더인지 확정 필요.
3. 빌드 OutputPath가 TDC를 가리키는 것이 **의도된 과도기 설정인지, 정리 대상(버그)인지** — 향후 PTOP 독립 출력(`_out_prismatic_rd_ptop`)으로 전환할지 결정 필요.
---
## 6. 진행 방향 (2026-06-18 결정)
- 어차피 **시작 프로젝트를 PTOP로 지정**하고, **언로드 중인 나머지(Scanner/Gun/CleanLaser/TDC)는 제외**하는 작업을 할 예정.
- 그 과정에서 PTOP `.csproj`의 빌드/실행 경로(현재 TDC 참조: `OutputPath`, `StartProgram`)도 **결국 PTOP 경로로 변경**할 것임 → §2의 TDC 참조는 그때 정리됨.
- 따라서 지금은 **분석만 남기고 복사는 보류**. (소스 무수정 원칙 유지)
- PTOP 단독 구성으로 전환 시: OutputPath/StartProgram을 `_out_prismatic_rd_ptop`로 바꾸고, 런타임 프로파일 폴더도 `ILSOS_PrismaticRD_PTOP/`로 생성/정리.

View File

@ -1,135 +0,0 @@
# PTOP 네임스페이스/클래스 정리 마이그레이션 분석 (Scanner → PTOP)
- **작성일:** 2026-06-18
- **대상:** `Machines/ILSOS_PrismaticRD_PTOP` (현재 작업본)
- **정답 참고본(빌드 성공):** `D:\Git\LGES-OC5-PtoP-Welder_정리후(빌드완)\Machines\ILSOS_PrismaticRD_PTOP`
- **목적:** 프로젝트명만 바꾸고 Sequence 일부만 PTOP로 바꾼 현재 상태를, 참고본처럼 **네임스페이스·클래스·파일명을 PTOP로 안전하게 정리**(빌드 깨짐 방지)하기 위한 분석.
---
## 0. 내 요청사항
> 현재 PTOP 프로젝트에 `using`이 SCANNER로 남아있는(=PTOP로 변경 안 한) 부분이 있어서, 수정하다 실수하면 빌드가 아예 안 될까 봐 걱정. Sequence만 바꾸고 프로젝트명만 바꿔둔 상태라 잘 수정하기 위한 분석이 필요. 잘 정리된 좋은 예시가 `D:\Git\LGES-OC5-PtoP-Welder_정리후(빌드완)`에 있음. 이런 형태로 수정하려고 함. Machines가 PTOP만 있는 것도 확인 가능.
---
## 1. 현재 상태 vs 참고본 (요약)
| 항목 | 현재(CUR) | 참고본(REF, 빌드완) |
|---|---|---|
| `Machines/` 구성 | PTOP + Scanner/Gun/CleanLaser(+_out들) | **PTOP 단독** |
| `namespace ILSOS_PrismaticScanner` | **26개 파일** (obj 4개 포함) | 0개 |
| `namespace ILSOS_PrismaticRD_PTOP` | 3개 파일만 (Properties 2 + SeqAutoRunPTOP) | 30개 파일 |
| `using ILSOS_PrismaticScanner.*` | **15개 파일** | 0개 |
| RootNamespace / AssemblyName | `ILSOS_PrismaticRD_PTOP` (이미 동일) | `ILSOS_PrismaticRD_PTOP` |
→ **csproj의 RootNamespace/AssemblyName은 이미 PTOP**인데, **실제 소스의 namespace/using/클래스명/파일명이 Scanner로 남아있는** 불일치 상태.
---
## 2. 변경 매핑 (참고본 기준 — 이대로 맞추면 됨)
### 2-1. 네임스페이스 (전 파일 공통)
- `namespace ILSOS_PrismaticScanner` → `namespace ILSOS_PrismaticRD_PTOP`
- `using ILSOS_PrismaticScanner.Alarm|Device|Device.IO|Models|Recipes|Settings|Sequences|Views;`
→ 동일 경로의 `ILSOS_PrismaticRD_PTOP.*` 로 변경
### 2-2. XAML (x:Class + clr-namespace) — 2개 파일
- `Views/Main/UCMainAutoRun.xaml`
- `Views/Model/UCEquipmentSpecificEditor.xaml`
- 각: `x:Class="ILSOS_PrismaticScanner.Views..."` → `ILSOS_PrismaticRD_PTOP.Views...`,
`xmlns:local="clr-namespace:ILSOS_PrismaticScanner.Views"` → `...ILSOS_PrismaticRD_PTOP.Views`
- ⚠ 짝꿍 `.xaml.cs`(code-behind)의 namespace도 함께 변경. `obj/.../*.g.cs`·`*.g.i.cs`는 **빌드시 자동 재생성**이므로 직접 수정 불필요(무시).
### 2-3. 파일명 + 클래스명 리네임 (참고본이 실제로 바꾼 것)
| 현재 파일 | 현재 클래스 | → 참고본 파일 | → 참고본 클래스 |
|---|---|---|---|
| `Alarm/AlarmCode_PrismaticScanner.cs` | `AlarmCode_PrismaticScanner` | `Alarm/AlarmCode_PTOP.cs` | `AlarmCode_PTOP` |
| `Alarm/AlarmManager_PrismaticScanner.cs` | `AlarmManager_PrismaticScanner` | `Alarm/AlarmManager_PTOP.cs` | `AlarmManager_PTOP` |
| `Device/IO/PAIX_PrismaticScanner.cs` | `PAIX_PrismaticScanner` | `Device/IO/PAIX_PTOP.cs` | `PAIX_PTOP` |
| `Models/EquipmentSpecificModel_PrismaticScanner.cs` | `EquipmentSpecificModel_PrismaticScanner` | `Models/EquipmentSpecificModel_PTOP.cs` | `EquipmentSpecificModel_PTOP` |
| `Recipes/RMSHandlerForScanner.cs` | `RMSHandlerForScanner` | `Recipes/RMSHandlerForPTOP.cs` | `RMSHandlerForPTOP` |
| `Settings/SettingUploadDataListForScanner.cs` | `SettingUploadDataListForScanner` | `Settings/SettingUploadDataListForPTOP.cs` | `SettingUploadDataListForPTOP` |
| `Sequences/SeqAutoRunPrismaticScanner.cs` | `SeqAutoRunPrismaticScanner` | `Sequences/SeqAutoRunPrismatic.cs` | `SeqAutoRunPrismatic` |
| `Device/PLC/PLCContoller.Config.cs` 외 2 (파일명 유지) | `PLCControllerPrismaticScanner` | (파일명 동일) | `PLCControllerPTOP` |
> PLC 3개 파일(`PLCContoller.Config.cs`, `PLCController.Address.cs`, `PLCController.Logic.cs`)은 **파일명은 유지**, 내부 **클래스명만 `PLCControllerPrismaticScanner` → `PLCControllerPTOP`**.
### 2-4. 클래스명 참조 전파 (find & replace 대상 파일 수, obj 제외)
| 심볼 | 영향 파일 수 | 비고 |
|---|---|---|
| `PLCControllerPrismaticScanner` | **13** | 가장 광범위 — 주의 |
| `PAIX_PrismaticScanner` | 5 | ⚠ **충돌 주의(아래 §3-1)** |
| `SeqAutoRunPrismaticScanner` | 5 | |
| `AlarmCode_PrismaticScanner` | 2 | |
| `AlarmManager_PrismaticScanner` | 2 | |
| `EquipmentSpecificModel_PrismaticScanner` | 2 | |
| `RMSHandlerForScanner` | 2 | |
| `SettingUploadDataListForScanner` | 2 | |
### 2-5. csproj `Compile Include` 경로 갱신 (파일명 변경분 7줄)
```
Alarm\AlarmCode_PrismaticScanner.cs → Alarm\AlarmCode_PTOP.cs
Alarm\AlarmManager_PrismaticScanner.cs → Alarm\AlarmManager_PTOP.cs
Device\IO\PAIX_PrismaticScanner.cs → Device\IO\PAIX_PTOP.cs
Models\EquipmentSpecificModel_PrismaticScanner.cs → Models\EquipmentSpecificModel_PTOP.cs
Recipes\RMSHandlerForScanner.cs → Recipes\RMSHandlerForPTOP.cs
Settings\SettingUploadDataListForScanner.cs → Settings\SettingUploadDataListForPTOP.cs
Sequences\SeqAutoRunPrismaticScanner.cs → Sequences\SeqAutoRunPrismatic.cs
```
---
## 3. 빌드 깨짐 유발 — 반드시 피해야 할 함정 ★
### 3-1. (최우선) `PAIX_PrismaticScanner` 이름 충돌 — **전역 일괄치환 금지**
- **머신 클래스**: `ILSOS_PrismaticScanner.Device.IO.PAIX_PrismaticScanner` → `PAIX_PTOP`로 변경 대상.
- **그러나 공유 클래스가 별도로 존재**: `WeldingCommon.Devices.Laser.PAIX_PrismaticScanner`
(선언: `WeldingComponents/WeldingCommon/Devices/Laser/LaserIOControl.cs`).
- 현재 `Sequences/SeqTestRun.cs:32` 가 **공유 쪽**을 참조: `using static WeldingCommon.Devices.Laser.PAIX_PrismaticScanner;`
- ⚠ `PAIX_PrismaticScanner` 를 **문자열 전역 치환하면 이 공유 참조까지 `PAIX_PTOP`로 바뀌어 컴파일 에러**. (WeldingCommon은 수정 금지 영역이기도 함.)
- **올바른 방법:** 머신 IO를 가리키는 4곳만 `PAIX_PTOP`로 변경하고, `SeqTestRun.cs`의 `WeldingCommon.Devices.Laser.PAIX_PrismaticScanner` 는 **그대로 둔다**.
- 변경 대상(머신): `EquipmentSetup.cs`(L153 생성/주석 L130), `Sequences/Manual/SeqComponentTeachingRun.cs`(L80/197 및 주석), `Sequences/Monitoring/SeqAlarmMonitor.cs`(L19 `using static ILSOS_PrismaticScanner.Device.IO.PAIX_PrismaticScanner` / L49 / L130).
- 유지(공유): `Sequences/SeqTestRun.cs:32`.
### 3-2. WeldingCommon / Aurora 등 공유 영역은 손대지 않는다
- 본 작업은 **PTOP 머신 프로젝트 내부에 한정**. 공유 프로젝트(WeldingCommon, AuroraDevices…)의 `PAIX_PrismaticScanner`, `RMSHandlerForScanner` 등 **동명 심볼이 있더라도 건드리지 않음**.
### 3-3. obj 생성물은 무시
- `obj/x64/Debug/Views/.../*.g.cs`, `*.g.i.cs`(현재 Scanner namespace) 는 **XAML 기준 자동 재생성**. 직접 수정 X. (clean 후 재빌드하면 갱신됨)
### 3-4. 짝(파트너) 누락 주의
- XAML 변경 시 **3종 세트**가 일치해야 함: `*.xaml`(x:Class) ↔ `*.xaml.cs`(namespace/class) ↔ csproj `<Page>`/`<Compile DependentUpon>`.
- 파일명 리네임 시 **csproj `Compile Include` 경로**도 같이 바꿔야 빌드 인식(§2-5).
### 3-5. 파일 리네임은 git 추적 유지
- `git mv` 로 리네임하여 히스토리 유지(클래스명/파일명 동시 변경).
---
## 4. 권장 안전 작업 순서
1. **백업/브랜치**: 별도 작업 브랜치에서 진행.
2. **파일 리네임**(`git mv`) 7개 → 클래스명 변경(파일 내부 선언부).
3. **namespace 일괄 변경**: `namespace ILSOS_PrismaticScanner` → `ILSOS_PrismaticRD_PTOP` (26 파일, obj 제외).
4. **using 일괄 변경**: `using ILSOS_PrismaticScanner.` → `using ILSOS_PrismaticRD_PTOP.` (15 파일).
5. **클래스명 참조 전파**: §2-4 심볼별로 **머신 참조만** 신규명으로 치환. ★ `PAIX_PrismaticScanner` 는 §3-1 규칙대로 공유 참조 1곳 제외.
6. **XAML 3종 세트** 변경(§2-2).
7. **csproj `Compile Include` 경로** 갱신(§2-5).
8. **clean → rebuild**. obj 재생성 확인. 참고본과 `namespace`/`using` 카운트(0 / 0) 일치 확인.
---
## 5. 검증 체크리스트 (작업 후)
- [ ] `grep -r 'namespace ILSOS_PrismaticScanner' (obj 제외)` → 0건
- [ ] `grep -r 'using ILSOS_PrismaticScanner'` → 0건
- [ ] `WeldingCommon.Devices.Laser.PAIX_PrismaticScanner` 참조(SeqTestRun.cs)는 **그대로 존재**
- [ ] csproj `Compile Include` 7줄이 새 파일명과 일치
- [ ] XAML x:Class / clr-namespace / code-behind namespace 3종 일치
- [ ] Debug|x64 리빌드 성공 (참고본과 동일 구성)
---
## 6. 참고: 참고본에만 있는 추가 파일(범위 밖, 참고만)
참고본에는 현재본 대비 `Sequences/SeqTestRun.cs`, `Sequences/Monitoring/SeqAlarmMonitor.cs`, `SeqStatusMonitor.cs` 등이 정리되어 있음. 본 마이그레이션의 핵심(네임스페이스/리네임)과 별개의 기능 정리이므로, 우선 §2~§5의 리네임 정합성에 집중.

View File

@ -1,117 +0,0 @@
# Manual Laser Power Control — "레이저 컨트롤러 OFF + 전압만 인가(NoLaser)" 분석 및 채택안
- **작성일:** 2026-06-18 (갱신: 최종 채택안 = IO Control DO9/13 + `IsNoLaser`)
- **관련 View/VM:**
- 채택 지점: `WeldingComponents/WeldingCommon/Views/Recipes/UCScannerObjectTeachingControlBox.xaml` (+ `...ViewModel.cs`)
- 초기 검토 지점(원복): `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControl.xaml` (+ VM)
- **디바이스:** `AuroraDevices/AuroraLaserScanner/Scanner.cs`, `ScannerRTC5.cs`, `WeldingCommon/Devices/Laser/LaserIOControl.cs`
- **선행 분석:** `102847_manual_laser_power_control_groupbox_analysis.md`
---
## 0. 내 요청사항
> Manual Laser Power Control 영역에서 **레이저(발진, lasing)는 안 나가게 하고 전압(Voltage)만 인가**되도록 하고 싶다. `Laser On`을 눌렀을 때 동작하도록 **체크박스/토글** 하나가 있어야 할 것 같다. (Cal 파일 작성용: 레이저는 안 쏘고 전압만 인가, 기록은 남기기)
>
> 추가 확인(현장): "Laser On 누르면 원래 **IO Control 9번이 true**가 되는데, 그 IO를 true로 안 켜면(false 유지) 발진 없이 동작한다." → 그 지점을 막는 방식으로.
---
## 1. 결론 요약 (최종 채택안)
- **실제 발진을 만드는 지점은 `LaserSignalOn`(RTC5 게이트)이 아니라 IO Control 비트였다.**
- **DO 9 = `CoreLaser_Control`**, **DO 13 = `RingLaser_Control`** (PAIX `ePrismaticDOAddress`).
- 이 둘이 true가 되면 레이저 컨트롤러가 발진 가능(enable) 상태가 됨.
- 따라서 **이 DO9/13을 켜는 부분을 NoLaser일 때 false로 유지**하면, 전압(WriteDA)은 인가되되 레이저 컨트롤러는 OFF → 발진 없이 `Laser On` 가능.
- 채택 위치: `UCScannerObjectTeachingControlBoxViewModel.PerformCmdLaserReadyCheck` (Laser Control GroupBox의 **Laser Ready** 버튼).
- 구현: ViewModel에 `IsNoLaser`(bool, 기본 false) 추가 + 켜는 분기에 `ctrlOn = !IsNoLaser` 적용 + XAML 체크박스.
---
## 2. 메커니즘 근거
### 2-1. 전압 출력과 발진은 독립 (선행 확인)
- `Scanner.WriteDA(port, value)` → RTC5 `n_write_da_x` (아날로그 전압 출력). `ScannerRTC5.WriteDA`: `targetValue = value * 40.95` (0~4095 = 0~10V). 즉 **출력 전압[V] = value/10**.
- `Scanner.LaserSignalOn()` → RTC5 `n_laser_signal_on` (스캐너측 레이저 Active 신호). 전압 출력과 별개 호출.
### 2-2. ★ 실제 레이저 컨트롤러 Enable = IO DO9/DO13
`WeldingCommon/Devices/Laser/LaserIOControl.cs` 의 `ePrismaticDOAddress`:
```
CoreLaser_Control = 9
CoreLaser_ErrorClearing
RingLaser_Control = 13
RingLaser_ErrorClearing
```
→ DO9/DO13(Core/Ring Laser Control)을 true로 주면 레이저 컨트롤러가 켜짐(발진 가능). **이게 "발진을 만드는 IO".**
(참고: DI9 = `CoreLaser_Laser_Ready`는 읽기 신호로 별개.)
### 2-3. DO9/13을 켜는 코드 위치
`UCScannerObjectTeachingControlBoxViewModel.PerformCmdLaserReadyCheck` — `Laser Ready` 버튼이 `toggleIO`로 DO9/DO13을 true/false 토글.
```csharp
if (toggleIO) {
_IODevice._iOUnit.WriteDoBit(9, true); // CoreLaser_Control ON
_IODevice._iOUnit.WriteDoBit(13, true); // RingLaser_Control ON
toggleIO = false;
} else {
_IODevice._iOUnit.WriteDoBit(9, false);
_IODevice._iOUnit.WriteDoBit(13, false);
toggleIO = true;
}
```
---
## 3. 채택 구현 (적용 완료)
### 3-1. ViewModel — `IsNoLaser` 추가
```csharp
private bool _IsNoLaser;
public bool IsNoLaser { get => _IsNoLaser; set => SetProperty(ref _IsNoLaser, value); }
// InitVariable()에서 IsNoLaser = false (기본: 기존 동작)
```
### 3-2. 켜는 분기를 NoLaser면 false로
```csharp
if (toggleIO) {
// IsNoLaser면 레이저 컨트롤러(BIT9/13)를 켜지 않음(false 유지) → 발진 없이 Laser On 가능
bool ctrlOn = !IsNoLaser;
_IODevice._iOUnit.WriteDoBit(9, ctrlOn);
_IODevice._iOUnit.WriteDoBit(13, ctrlOn);
toggleIO = false;
} else {
_IODevice._iOUnit.WriteDoBit(9, false);
_IODevice._iOUnit.WriteDoBit(13, false);
toggleIO = true;
}
```
### 3-3. XAML — 체크박스 (Laser Control GroupBox)
```xml
<CheckBox Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Margin="5 6 0 0"
FontSize="12" FontWeight="Bold" Foreground="#FFBBBBBB"
IsChecked="{Binding IsNoLaser}"
Content="No Laser (전압만, 발진 안 함)"/>
```
### 3-4. 동작 표
| IsNoLaser | Laser Ready 버튼 | 결과 |
|---|---|---|
| **false (기본)** | DO9/13 ON/OFF 토글 | 기존과 동일(레이저 컨트롤러 켜짐 → 발진 가능) |
| **true (NoLaser)** | DO9/13을 **false 유지** | 레이저 컨트롤러 OFF, 발진 없이 진행(전압만) |
> 적용 = 커밋 `8ec4fd0`. 디바이스 레이어(Scanner/RTC5) 무변경, IO 비트 9/13만 제어.
---
## 4. 초기 검토안(원복) 기록
- 처음엔 `UCScannerLaserControl`(Manual Laser Power Control)에 `IsAnalogOnly`를 추가해 `LaserSignalOn()`(RTC5 게이트)을 조건부로 막는 방식을 구현했음.
- 그러나 현장 확인 결과 **실제 발진 Enable은 다른 소스(ObjectTeachingControlBox)의 IO DO9/13**임이 확인되어, 해당 변경은 **원복**(Watt 숨김 커밋 `ad6cf68`은 유지)하고 본 §3의 DO9/13 방식으로 재구현함.
- 교훈: "발진을 막는 지점"이 RTC5 게이트(`LaserSignalOn`)가 아니라 **레이저 컨트롤러 Enable IO(DO9/13)** 였음.
---
## 5. 주의 / 추가 확인
1. **전압 표시(요청)**: 인가 전압을 화면에 표시하려면, 하드웨어 readback API는 없으므로 **명령 전압값(= WriteDA에 쓰는 powerVolt/10 [V])** 을 VM 프로퍼티로 노출해 표시하는 방식 권장(디바이스 무변경). → 별도 작업으로 진행 가능.
2. **RTC5 기록 저장**: WriteDA(전압)는 발진과 무관하게 수행되므로 전압 명령 자체는 남음. "기록 저장"이 앱 로깅을 의미하면 별도 로깅 경로 확인 필요.
3. **안전**: NoLaser는 기본 false. 체크 상태를 화면에 명확히 표시(체크박스 라벨). NoLaser일 때는 DO9/13이 절대 true로 가지 않음(켜는 분기·끄는 분기 모두 false).
4. **Manual Laser Power Control(UCScannerLaserControl) 쪽**: 현재 Watt GroupBox는 숨김(`Collapsed`), Analog Only 변경은 원복됨. 추후 그쪽에도 동일 NoLaser 연동이 필요한지는 운영하며 판단.

View File

@ -1,90 +0,0 @@
# "Laser On 눌러도 레이저가 안 나옴" 원인 분석 (기능 존재 여부)
- **작성일:** 2026-06-18
- **대상:** `WeldingComponents/WeldingCommon/Views/Manual/Scanner/UCScannerLaserControlViewModel.cs` (`PerformCmdLaserShotOnPercent` / `...Watt`)
- **디바이스:** `AuroraDevices/AuroraLaserScanner/Scanner.cs`, `ScannerRTC5.cs` (Trumpf TruFiber 레이저)
- **선행:** `102847_...groupbox_analysis.md`, `114445_...no_laser_toggle_analysis.md`
---
## 0. 증상 / 질문
> IO Control(DO9/13)도 true, DI(Laser Ready)도 잘 읽혀서 Laser Ready도 true → 레이저가 나와야 하는데 **Laser On 눌러도 레이저가 안 나온다.** 기능이 현재 없는 건지 분석.
---
## 1. 결론
- **준비조건(IO/Ready)은 문제 없음.** 문제는 발사 명령 자체에서 **레이저 Gate 신호 셋업이 빠져있는 것**.
- **Trumpf 레이저는 `SetLaserTiming`으로 Gate 신호를 살려줘야 출사된다**(코드 주석으로 명시 + 241025 출사 확인 기록).
- 그런데 수동 **Laser On 직접 분기(No Use Spot Time)** 에서는 `SetLaserTiming`(과 `SetStandby`)이 **주석 처리**되어 있어, `LaserSignalOn()`만으로는 Gate가 안 열려 **실제 출사가 안 됨.**
- 즉 "직접 출사" 기능은 **사실상 미완성(셋업 라인 비활성)** 상태. 기능 자체가 아예 없는 건 아니고, **Gate 셋업 한 줄이 빠진 것**.
---
## 2. 근거
### 2-1. Laser On 직접 분기 (현재 코드, `PerformCmdLaserShotOnPercent` else 분기)
```csharp
_Scanner.WriteDA(DA1, powerVolt1, COMMAND); // 전압 OK
_Scanner.WriteDA(DA2, powerVolt2, COMMAND);
_Scanner.SetLaserPulseCtrl(LaserPeriod*1000, LaserPulseWidth);
//_Scanner.SetLaserTiming(LaserPeriod*1000, LaserPulseWidth, LaserPulseWidth); // ← 주석!
//_Scanner.SetStandby(LaserPeriod*1000, LaserPulseWidth); // ← 주석!
_Scanner.LaserSignalOn(); // Gate(LASERON) 신호만 올림
```
→ `LaserSignalOn()`은 RTC5 `n_laser_signal_on`(레이저 Active 신호)만 올림. **Gate 타이밍(SetLaserTiming) 설정이 없으면 Trumpf 레이저는 출사 안 됨.**
### 2-2. "Trumpf는 SetLaserTiming으로 Gate 살려야 함" 명시 (SeqUploadWeldingList.cs:624~626)
```
//TrumpLaser의 경우 SetLaserTiming 함수로 Gate 신호를 살려줘야 함
//우선 무조건 셋팅하는 것으로 쓰고 Trump_IO Class 만들면 ... Trump의 경우에만 셋팅
//LaserFrequency = 10, LaserPulseDuration = 0.1로 셋팅하면 출사 되는 것 확인_241025
```
→ **출사 확인된 셋업: `SetLaserTiming(주파수=10, PulseDuration=0.1ms)`** (= `SetLaserTiming(10, 100, 100)` 형태).
### 2-3. 실제 출사되는 경로는 List + Timing 셋업 사용
- `SeqUploadWeldingList` / `SeqMarkGrid` 등 실제 마킹 경로는 `SetLaserTiming(...)` + `LaserOnList(...)`(리스트 기반)로 출사.
- 수동 버튼 직접 분기는 리스트가 아닌 `LaserSignalOn` 직접 호출인데 Timing 셋업이 빠짐.
---
## 3. 두 가지 Laser On 경로 정리
| 모드(토글) | 경로 | 출사 여부 |
|---|---|---|
| **Use Spot Time** (기본값 `IsUseLaserAutoOffChecked=true`) | `SeqTeachingRun.Start(...)` (리스트 기반, `NeedSetLaserTiming=true` 전달) | 모델 선택 등 전제 충족 시 출사(스팟). 단 모델 null/Busy/LaserReady false면 경고 후 미발사 |
| **No Use Spot Time** (직접) | `WriteDA → SetLaserPulseCtrl → LaserSignalOn` | **SetLaserTiming 주석 → Gate 안 열림 → 출사 안 됨** ★ |
> 사용자가 "직접 한 방 쏘려고" No Use Spot Time로 눌렀다면 §2-1 이유로 안 나옴.
> Use Spot Time(기본)인데도 안 나온다면 §3 표의 전제(모델 선택/Busy/`_Laser.GetLaserReady()`)에서 막혔을 가능성(토스트 메시지 확인 필요).
---
## 4. 권장 수정 (직접 분기 출사 활성화)
직접 분기에서 `LaserSignalOn()` **앞에** Gate 타이밍을 살려준다:
```csharp
_Scanner.SetLaserPulseCtrl(LaserPeriod*1000, LaserPulseWidth);
_Scanner.SetLaserTiming(LaserPeriod, LaserPulseWidth, LaserPulseWidth); // ← 주석 해제/추가 (Gate 신호 enable)
_Scanner.LaserSignalOn();
```
- 우선 검증은 **확인된 값**으로: `SetLaserTiming(10, 100, 100)`(주파수 10, PulseDuration 0.1ms) → 출사 확인된 셋업(241025).
- 그 후 화면 입력값(`LaserPeriod`/`LaserPulseWidth`) 단위를 SetLaserTiming 인자 단위(periodHz, pulseWidthUs)에 맞춰 매핑 필요.
- Laser Off 시 `LaserSignalOff()` 유지(이미 있음).
> ⚠ 안전: 이 수정은 **실제 레이저를 출사**시키므로, 적용/테스트는 차폐·인터락·저파워에서 단계적으로. (NoLaser 옵션과 함께 쓰면 컨트롤러 OFF로 무발광 검증 가능.)
---
## 5. 추가 확인 필요
1. 사용자가 누른 Laser On이 **어느 모드(Use Spot Time vs No Use Spot Time)** 였는지 → 토스트 메시지/화면 토글 상태 확인.
2. `SetLaserTiming` 인자 단위(periodHz/us)와 화면 입력값(`LaserPeriod` kHz, `LaserPulseWidth` us) 정합 매핑.
3. 현재 레이저 타입(`_LaserConfig.TruFiberType`)이 Trumpf 계열인지(맞으면 Gate 셋업 필수).
4. Use Spot Time 경로가 막히는 경우 전제조건(`_Laser.GetLaserReady()`, 모델 선택, Scanner Busy) 로그 확인.
---
## 6. 답: "기능이 없는 건가?"
- **완전히 없는 건 아님.** Use Spot Time(시퀀스) 경로는 출사 로직이 있음.
- 다만 **직접 출사(No Use Spot Time) 경로는 Gate 셋업(`SetLaserTiming`)이 주석 처리되어 미완성** → 그래서 IO/Ready가 다 맞아도 안 나옴. §4 한 줄(또는 확인값) 추가가 핵심.

View File

@ -1,106 +0,0 @@
# Monitoring IOInterfaceView DI 파일 경로 — PTOP 바라보게 하기 분석
- **작성일:** 2026-06-18
- **목표:** 작업환경에서 IOInterfaceView가 `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_device\_io` 의 DI 파일을 보게 하기
- **대상:** `AuroraDevices/AuroraIO/IODevice.cs`, `AuroraDevices/AuroraIO/View/UCIOInterfaceViewModel.cs`, 설정 `IO_Board.json`
---
## 0. 요청
> 마이그레이션 전(현장 소스)에는 PTOP 프로젝트 시작이어도 Monitoring의 IOInterfaceView가 `D:\ILSOSConfig\ILSOS_PrismaticScanner\_device\_io` 의 DI 파일을 참조해 보여줬다. 내 작업환경에서 **PTOP(`D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP`)** 데이터를 보게 하려면 어디를 고쳐야 하나? 수정 위치 + diff. 문제 없으면 그 이유도.
---
## 1. 결론 (요약)
- **소스 코드는 고칠 필요가 없다(IO DI 한정).** DI 파일 경로는 **소스에 없고**, **설정 파일 `IO_Board.json` 의 `IOInterfaceMapDI`(절대경로)** 값으로 결정된다.
- `IODevice.MappingDIO()`는 그 절대경로를 **그대로 읽는다**(DataPath 등 가공 없음). 즉 **경로 무관(path-agnostic)** 코드 → 버그 없음.
- 따라서 PTOP를 바라보게 하는 건 **설정/환경 변경**:
1. 앱이 로드하는 config 폴더(DataPath)가 `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP` 여야 함.
2. 그 폴더의 `_config_device\IO_Board.json` 의 `IOInterfaceMapDI`/`IOInterfaceMapDO` 가 **PTOP 경로**를 가리켜야 함.
3. 실제 `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_device\_io\DI.csv`(DO.csv) 파일이 존재해야 함.
---
## 2. 경로 결정 메커니즘 (코드 근거)
### 2-1. DI/DO 매핑 로드 — `AuroraDevices/AuroraIO/IODevice.cs` `MappingDIO()`
```csharp
var m_connConfig = _connConfig as IOUnitConnConfig;
switch (type) {
case InfoIO.IoType.DI: fileName = m_connConfig.IOInterfaceMapDI; break; // ← 설정값(절대경로)
case InfoIO.IoType.DO: fileName = m_connConfig.IOInterfaceMapDO; break;
}
FileInfo fi = new FileInfo(fileName);
if (!fi.Directory.Exists) fi.Directory.Create();
if (File.Exists(fileName) == false) {
Log.I($"파일({fileName})이 존재하지 않습니다. Device Config의 File 경로를 확인하세요.");
return 0; // ← 없으면 로그만, 크래시 없음(빈 리스트)
}
StreamReader sr = new StreamReader(fileName, ...); // ← 절대경로 그대로 읽음
... ListDI/ListDO 채움
```
→ **`fileName`은 `IOInterfaceMapDI` 값 그 자체.** 코드가 폴더명을 만들거나 바꾸지 않음.
### 2-2. 설정 출처 — `IO_Board.json` (`_config_device`)
`Doc/ILSOSConfig/ILSOS_PrismaticScanner/_config_device/IO_Board.json`:
```json
"IOInterfaceMapDI": "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_device\\_io\\DI.csv",
"IOInterfaceMapDO": "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_device\\_io\\DO.csv",
```
→ **여기 절대경로가 `ILSOS_PrismaticScanner`로 박혀 있어서**, 어떤 config 폴더를 로드하든 이 값이면 Scanner DI를 읽었던 것.
### 2-3. 화면 — `UCIOInterfaceViewModel`
ViewModel은 위에서 로드된 `_iOUnit.ListDI`를 표시할 뿐, 경로를 직접 다루지 않음. → 화면 쪽도 수정 불필요.
### 2-4. 앱이 어느 config 폴더를 로드하는가 (DataPath)
- `D:\ILSOSConfig\<폴더>` 의 `<폴더>` 는 Aurora 프레임워크(`ApplicationExt.DataPath`, External\Aurora 바이너리)가 결정. **repo 소스에 없음.**
- 관례상 머신 플러그인 **AssemblyName** 과 일치(과거 `ILSOS_PrismaticScanner`). 이번 마이그레이션으로 **AssemblyName = `ILSOS_PrismaticRD_PTOP`** 이 되었으므로, DataPath도 `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP` 로 해석될 가능성이 큼 → **그 폴더가 준비돼 있어야 함**(없으면 §2-1처럼 "파일 없음" 로그 후 빈 표시).
---
## 3. 수정 위치 (= 설정/환경, 소스 아님)
### (필수) IO_Board.json 경로 값 — `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_config_device\IO_Board.json`
```diff
- "IOInterfaceMapDI": "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_device\\_io\\DI.csv",
- "IOInterfaceMapDO": "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_device\\_io\\DO.csv",
+ "IOInterfaceMapDI": "D:\\ILSOSConfig\\ILSOS_PrismaticRD_PTOP\\_device\\_io\\DI.csv",
+ "IOInterfaceMapDO": "D:\\ILSOSConfig\\ILSOS_PrismaticRD_PTOP\\_device\\_io\\DO.csv",
```
### (필수) 데이터 파일 존재
`D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\_device\_io\DI.csv`, `DO.csv` 가 실제로 있어야 함(Scanner 폴더에서 복사 또는 260618 zip 사용).
### (확인) DataPath
앱 실행 시 로드되는 config 루트가 `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP` 인지 확인(로그의 파일 경로/“파일 없음” 메시지로 검증).
> **즉 IO DI에 대한 C# 소스 diff 는 없음.** §2-1 코드가 path-agnostic 이라 고칠 게 없다(= "문제 없는 이유").
---
## 4. "문제 없는 이유" 정리
- `MappingDIO`는 `IOInterfaceMapDI` 절대경로를 **그대로** `StreamReader`로 연다. 경로 조립/머신명 치환 로직이 없음 → **코드가 머신에 종속되지 않음.**
- 파일이 없으면 예외 없이 로그 후 빈 리스트(`return 0`) → IOInterfaceView가 비어 보일 뿐 크래시 없음.
- 그래서 "PTOP를 바라보게" 하는 건 코드 수정이 아니라 **(1) PTOP config 폴더 준비 + (2) 그 안 IO_Board.json 경로를 PTOP로 + (3) DI/DO.csv 존재**로 끝난다.
---
## 5. (참고) 별개 이슈 — 소스에 하드코딩된 `ILSOS_PrismaticScanner` 절대경로
IO DI와 무관하지만, **PTOP config로 완전 전환** 시 같이 손봐야 할 하드코딩 경로(공유 소스, WeldingCommon/LASA — 수정 시 승인 필요):
| 파일 | 용도 |
|---|---|
| `WeldingCommon/Settings/SystemParameter.cs:17,18` | FieldScanImage, WORK_RECIPE_FILE |
| `WeldingCommon/RMS/EDC.cs:171,177` | cellTracker CSV |
| `LASA/GUI/DB/RecipeFieldScan.cs:24` | 기본 Scout 이미지 |
| `LASA/GUI/Page/MainPage.cs:259,533` | 기본 FieldScan 이미지 |
| `WeldingCommon/Views/Manual/Scanner/WDSelectScoutFieldViewModel.cs:47` | 기본 FieldScan 이미지 |
> 이들은 모두 `D:\ILSOSConfig\ILSOS_PrismaticScanner\...` 로 박혀 있음. **IOInterfaceView DI 표시에는 영향 없음**(vision/recipe/cellTracker 기본값). 필요 시 `ILSOS_PrismaticRD_PTOP`로 일괄 치환하는 별도 작업으로 진행.
---
## 6. 권장 조치 (작업환경)
1. `D:\ILSOSConfig\ILSOS_PrismaticRD_PTOP\` 준비(없으면 Scanner 복사 또는 260618 zip 해제).
2. `…\_config_device\IO_Board.json` 의 DI/DO 경로를 §3 diff 대로 PTOP로 변경.
3. `…\_device\_io\DI.csv`, `DO.csv` 존재 확인.
4. 앱 실행 → IOInterfaceView에 PTOP DI 표시 확인. (안 보이면 로그의 파일 경로로 DataPath/파일유무 점검)

View File

@ -1,111 +0,0 @@
# ILSOS PTOP 알람부(AlarmManager) 종합 분석 — 2026-06-19
> 계기: 현장 PLC 미연결 상태에서 `SeqComponentTeachingRun` 시작 시 H(중)알람 인터락(202003)으로 차단됨.
> 본 문서는 알람부 전체 구조·동작·시퀀스 참조·리스크·개선안을 정리한다.
---
## 1. 클래스 구조
| 클래스 | 위치 | 역할 |
|--------|------|------|
| `AlarmManager` (base) | `WeldingCommon/Alarm/AlarmManager.cs` | 정적 싱글톤(`_Instance`). 알람 set/조회/시퀀스 정지 |
| `AlarmManager_PTOP` | `PTOP/Alarm/AlarmManager_PTOP.cs` | `StopSequenceByAlarm` — H알람 시 AutoRun 정지 |
| `AlarmCode` / `AlarmCode_PTOP` | `.../Alarm/AlarmCode*.cs` | 알람 코드 상수. PTOP는 base 상속 후 21xxxx 추가 |
| `IAlarmService` (Aurora 프레임워크) | External/Aurora | **활성 알람 목록·설명·레벨(H/L)** 보유 |
- 알람 **레벨(H/L)** 은 소스 상수가 아니라 **Aurora 런타임 알람 테이블**에 정의됨(코드에 `AlarmLevel.H` 할당부 없음).
## 2. 핵심 메서드 동작 (주의점)
- **`Alarm(code)`** (79줄): 이미 활성이면 재set 안 함 → **set-once(중복 없음)**.
- **`HasAlarmLevelHigh()`** (172줄): 활성 알람 중 H 하나라도 있으면 true. **⚠️ Simulation이면 무조건 false**(시뮬은 인터락 우회).
- **`IsAlarmLevelHigh(code)`** (194줄): 설명 모르는 코드(`desc==null`)는 **위험으로 보고 H 취급** → 미정의 코드도 인터락 유발.
- **Clear**: 개별 `ClearAlarm()`은 **빈 함수**(주석만). 실제 해제는 **`ClearAllAlarm()`(전체 일괄)** 뿐.
- 트리거: `SYS_ALARM_RESET_ALL_ALARM` / `EVENT_RESET_ALL_ALARMS_REQUEST` → `_AlarmService.ClearAllAlarm()`.
- **PLC 알람워드 직접쓰기**(`WriteBit(HeavyAlarmStatusAddr..)`)는 AlarmManager 내에서 전부 **주석 처리**됨(대신 SeqStatusMonitor가 수행).
## 3. 알람 생성·소멸·전파 흐름
```
[생성] SeqAlarmMonitor (상시 주기 감시)
├ PLC: EMO / Heavy / Door / 통신끊김
├ Laser/Scanner/LBAD/OCT/Chiller: Deactivated/Disconnect/Error
└ IO: Leak / null / deactivated
→ AlarmManager.Alarm(code) ← set-only, ❌조건 해소 시 Clear 안 함(LATCH)
[전파] EquipmentSetup.OnSetAlarm(code, isHeavy) (EquipmentSetup.cs:82)
→ isHeavy 면 AlarmManager_PTOP.StopSequenceByAlarm(code) → H면 StopSequence(SeqAutoRun) 비상정지
[PLC보고] SeqStatusMonitor.UpdateStatus (SeqStatusMonitor.cs:86)
→ PC_HEAVY_ALARM_STATUS = HasAlarmLevelHigh(), PC_LIGHT_ALARM_STATUS = HasAlarmLevelLow()
→ PLC_ALARM_RESET 입력 시 EVENT_RESET_ALL_ALARMS_REQUEST 발행 → ClearAllAlarm()
[차단] 각 시퀀스 시작 시 CheckCondition_Interlock() → HasAlarmLevelHigh() H면 시작 거부
```
## 4. 주요 시퀀스별 알람 참조
| 시퀀스 | 인터락 체크 | 비고 |
|--------|------------|------|
| **SeqComponentTeachingRun** | `HasAlarmLevelHigh()` → "Clear Heavy Alarms" + 202003 (Door/EMO 주석) | **이번에 막힌 시퀀스** |
| SeqTeachingRun (Common) | `CheckCondition_Interlock()` = **빈 체크(true)** | 알람 체크 없음 |
| **SeqAutoRunPTOP** | `CheckCondition_Interlock()` = **빈 체크(true)** | 차단은 시작이 아니라 OnSetAlarm→StopSequence |
| SeqAutoRunPrismatic / ComponentWeld | 동일 패턴(빈 체크) | |
| ScoutNormal/FlyingVision, UploadWeldingList, ReceiveCellInfo, ReportCellResult, MarkGrid, ScannerCalibration | 대부분 빈 체크 | 공통 시퀀스 |
| **SeqStatusMonitor** | 알람을 **읽어 PLC 출력** + Reset 처리 | 차단 아님 |
| **SeqAlarmMonitor** | 알람을 **생성(set-only)** | latch 원인 |
> 실제 알람으로 "시작 차단"하는 곳은 `SeqComponentTeachingRun`이 거의 유일. AutoRun 계열은 "운전 중 H알람 발생 시 비상정지" 방식.
## 5. 알람 코드 표 (추정 레벨 — 권위값은 런타임 테이블)
| 코드 | 이름 | 용도 | 추정 |
|------|------|------|------|
| 200xxx~203xxx | ALARM_SEQUENCE_* | 시퀀스 결과/초기조건 실패 | 혼합 |
| 203004 | AutoRun_HeavyAlarmOccured | 자동운전 중 중알람 | H |
| 210001/2/3 | PLC Deactivated/Disconnect/**EMO** | PLC 상태/비상정지 | H |
| 210004/5/6 | PLC Light / **Heavy** / Door Interlock | PLC 경/중/도어 | 210005·6 H, 210004 L |
| 210101/2/3 | Scanner Deactivated/Disconnect/Error | 스캐너 | H/혼합 |
| 210201~205 | Laser Deactivated/Disconnect/Error/Warning/**EMO** | 레이저 | 205·201 H, 204 L |
| 210206/7 | Laser Warning/Error MSG | 레이저 메시지(동적) | L/H |
| 210301~303 | LBAD Deactivated/Disconnect/Error | LBAD | H/혼합 |
| 210401~403 | OCT Deactivated/Disconnect/Error | OCT | H/혼합 |
| 210501~503 | Chiller Deactivated/Disconnect/Error | 냉각기 | H/혼합 |
| 210600~604 | IO Null/Deactivated/Disconnect/**Leak**(Scanner/Chiller) | IO보드/누액 | H |
| 210605~614 | IO MC/BoxTemp/ChillerLeak/Fan1~6 | (현재 대부분 주석) | 혼합 |
| 210700~705 | RMS_* 타임아웃/설정 | 레시피 관리 | L 추정 |
## 6. 리스크 / 개선 포인트
1. **알람 Latch (최우선)** — `SeqAlarmMonitor`가 set만 하고 조건 정상화 시 `ClearAlarm` 안 함. → **이번 PLC 미연결 사례**가 정확히 이것: 연결 복구돼도 알람이 남아 차단, 수동 ClearAll 필요.
2. **개별 Clear 부재** — `ClearAlarm()`이 빈 함수 → 전체 일괄 해제만 가능, 특정 알람만 해제 불가.
3. **Unknown=H 취급** — 미정의 코드 활성 시 무조건 H 차단 → 알람 테이블 누락 시 의도치 않은 인터락.
4. **시뮬 우회** — `HasAlarmLevelHigh()`가 Simulation이면 false → 시뮬/현장 동작 차이.
5. **인터락 일관성 부재** — TeachingRun만 알람 차단, 나머지 시작 시퀀스는 빈 체크. 안전 정책 제각각.
6. **Door/EMO 주석** — SeqComponentTeachingRun·UCMainAutoRunViewModel의 Door/EMO 인터락 주석 → 현재 중알람 1종만 차단.
## 7. 개선 제안 — SeqAlarmMonitor self-clearing
각 검사 항목을 "조건 나쁨 → Alarm, 조건 좋음 → Clear" 대칭 구조로. 예(개념):
```csharp
// AS-IS
if (!_Laser.IsActivated) AlarmManager.Alarm(ALARM_DEVICE_LASER_DEACTIVATED);
else { /* clear 없음 → latch */ }
// TO-BE (self-clearing) — AlarmManager에 개별 ClearAlarm(code) 구현 전제
if (!_Laser.IsActivated) AlarmManager.Alarm(ALARM_DEVICE_LASER_DEACTIVATED);
else AlarmManager.ClearAlarm(ALARM_DEVICE_LASER_DEACTIVATED);
```
- 전제: `AlarmManager.ClearAlarm(int code)` 실구현 필요(현재 빈 함수). `_AlarmService`에 단건 해제 API 확인 후 연결.
- 효과: PLC 재연결 등 조건 복구 시 알람 자동 해제 → 수동 ClearAll 불필요, 인터락 latch 해소.
- 범위: AlarmManager(공통) 수정 → **승인 필요 영역**. 적용 전 별도 협의.
## 8. 부록 — 이번(PLC 미연결) 사례 요약
- PLC 미연결 → `SeqAlarmMonitor`가 PLC Deactivated/Disconnect(H) set → latch.
- `SeqComponentTeachingRun` 시작 → `HasAlarmLevelHigh()` true → 202003, FORCE_ERROR.
- 임시조치: `SeqComponentTeachingRun.CheckCondition_Interlock()`의 H알람 블록 주석(테스트용, **원복 예정**) → 레이저 출사 확인.
- 관련: [[axis-position-interlock-todo]]

View File

@ -1,82 +0,0 @@
# MX Component PLC 연결 설정 분석 — 2026-06-19
> 계기: 현장 PLC 미연결 → 중알람 latch → TeachingRun 인터락 차단.
> PLC를 정상 연결하려면 무엇을 PLC측에 요청하고, PC 소스/설정의 어떤 값을 맞춰야 하는지 분석(수정 없음).
> 관련: [[110333_alarm_subsystem_analysis]]
---
## 1. 연결 메커니즘 (소스 기준)
- 드라이버: `AuroraDevices/AuroraMXComponent/MXComponent.cs` — Mitsubishi **MX Component** .NET 컨트롤 `ActUtlType64Class` 사용.
- PTOP 등록(`EquipmentSetup.cs:158`):
`new PLCControllerPTOP(PLCDevice, ePLCNetType.MXComponent, IsSimulation, PLCMMapFileType.CSV, AuroraEnv)`
- 연결 흐름:
```
PLCBase.DoActivate (PLCBase.cs:80)
→ NetworkType == MXComponent
→ PLCNet.SetInitParams(connConfig.StationNumber) // = ActLogicalStationNumber
→ Open() → _actUtlType.ActLogicalStationNumber = N; _actUtlType.Open()
```
- **핵심 파라미터 = `StationNumber` (논리국번 / Logical Station Number)**.
`MXComponent.SetInitParams(int logicalStationNo)` → `_actUtlType.ActLogicalStationNumber = logicalStationNo` (MXComponent.cs:26~29).
- Open 결과: `0x00000000` 성공, 그 외 Hex 에러. `0x0180840B`(타임아웃) 시 1회 재시도(MXComponent.cs:48).
## 2. 맞춰야 하는 값 — PC측
### (A) MX Component "Communication Setup Utility" (별도 PC 도구)
- MX Component 설치 시 함께 깔리는 **통신 설정 유틸리티**에서 **Logical Station Number** 를 등록.
- 등록 시 입력: PLC **CPU 기종**, **접속 경로**(CPU 내장 이더넷 / 이더넷 모듈 / CC-Link IE / USB / 시리얼), **PLC IP/포트**, 네트워크/국번.
- 이 유틸리티에 등록된 논리국번이 실제 통신 경로를 정의함. (소스는 "번호"만 들고, 실 경로는 유틸리티가 보유)
### (B) 소스/설정 파일 — `StationNumber` 일치
- 정의: `AuroraDevices/AuroraPLC/PLCBase.Config.cs:37`
`PLCMXComponentConnConfig.StationNumber` 기본값 **1** (주석: "Logical station No. (By PC Tool: Communication Setting Utility)").
- 실제 값은 **디바이스 config 파일에 저장**: `_config_device/PLC.json`
```json
{ "ConnConfig": { "StationNumber": 22, "UpdateRateMS": 50 } }
```
(위는 Gun 예시 = 22. PTOP는 현장 값으로 별도 설정 필요)
- **편집 경로**: 프로그램 내 PLC 디바이스 설정 화면(`UCPLCDevice`)에서도 수정 가능, 또는 위 JSON 직접 편집.
- ⚠️ **(A)의 유틸리티 논리국번 == (B)의 PLC.json StationNumber** 이어야 연결됨. (현장 PLC.json은 repo에 없음 = 현장 전용)
## 3. PLC측에 요청해야 할 것
| 항목 | 내용 |
|------|------|
| CPU 기종 | Q / L / iQ-R / iQ-F 등 정확한 모델 |
| 접속 방식 | CPU 내장 이더넷 / 이더넷 모듈 / CC-Link IE / USB / 시리얼 중 무엇 |
| 네트워크 정보 | PLC **IP 주소 / 포트**, 서브넷, (CC-Link IE면) 네트워크No·국번 |
| MELSOFT 접속 허용 | 이더넷 "MELSOFT 접속" 오픈 설정, 포트 개방(보통 5007/5006 등), 방화벽 |
| 국번 구성 | 멀티 CPU/증설 시 대상 station |
→ 위 정보를 받아 **(A) Communication Setup Utility에 논리국번으로 등록**하고, 그 번호를 **(B) PLC.json StationNumber** 에 동일하게 넣으면 연결.
## 4. 메모리맵 CSV (연결과 별개, 데이터 정합용)
- `PLCMMapFileType.CSV` → `PLCBase.MemMapParser.cs`가 아래 8종 CSV를 읽어 디바이스 주소 매핑:
- `External/PLC/PLC_DeviceMap_Bit_Input_B_Device.csv` / `..._Output_B_Device`
- `..._Bit_Input_M_Device` / `..._Output_M_Device`
- `..._Word_Input_W_Device` / `..._Output_W_Device`
- `..._Word_Input_D_Device` / `..._Output_D_Device`
- 경로: recipePath 기준 `External\PLC\...` (현장 config 폴더).
- 연결(Open)과는 무관하지만, 연결 후 **읽고/쓰는 주소가 PLC 실제 디바이스와 맞아야** 알람/인터페이스가 정상. PTOP는 repo에 맵 CSV 없음(현장 전용) → [[plc-devicemap-260617-pending]] 참고.
## 5. 점검 순서 (현장)
1. PLC측에서 **CPU기종·접속방식·IP/포트·MELSOFT 접속 허용** 확보
2. PC에서 **MX Component Communication Setup Utility** 실행 → 위 정보로 **논리국번**(예: 1) 등록 → 유틸리티 내 "통신 테스트" 통과 확인
3. `_config_device/PLC.json` 의 `StationNumber` 를 그 논리국번과 동일하게 설정 (또는 PLC 디바이스 설정 화면)
4. 프로그램 재기동 → PLC Activate → 로그 `PLC Open success` 확인 (`Open: 0x00000000`)
5. 연결되면 SeqAlarmMonitor의 PLC Deactivated/Disconnect 알람 조건 해소 → (단, latch라 **ClearAll 1회** 필요할 수 있음, [[110333_alarm_subsystem_analysis]] 참고)
## 6. 참고 — 에러코드 / 매뉴얼
- Open/Close 반환값은 MX Component 표준 Hex 에러코드. `0x0180840B` = 타임아웃(소스에서 1회 재시도).
- 상세 절차·에러표: `Doc/메뉴얼/공통/PLC/21. MX_Component_V4_프로그래밍_매뉴얼.pdf` (Communication Setup Utility / ActUtlType / 에러코드 장 참조).
## 7. 요약
- **연결 핵심 = 논리국번 1개**. PC의 MX Component 유틸리티 등록값 ↔ 소스 `PLC.json StationNumber` 일치 + PLC측 이더넷/접속 허용.
- PLC.json·맵 CSV는 **현장 전용**(repo 없음) → 현장에서 값 세팅 필요.
- 연결되면 PLC 미연결 중알람 해소 → TeachingRun 인터락 정상화 (latch면 ClearAll 1회).

View File

@ -1,68 +0,0 @@
# AjinIO.ReadDoBit(int) 미구현 분석 — 2026-06-19
> 계기: `AjinIO.cs`의 `ReadDoBit(int index)`(bool 반환)가 미구현 스텁(`return true`).
> DI 패턴 대비 분석 후, 현 상태는 **스텁 유지 + 구현 주석 보존**으로 결정(현장 반영 예정).
> 관련: [[110333_alarm_subsystem_analysis]]
---
## 1. 현황
- 파일: `AuroraDevices/AuroraIO/Vendor/AJINEXTEK/AjinIO.cs` (보호 영역, 신중 수정)
- DI/DO 오버로드 4종(`IOUnit` 추상):
- `int ReadDoBit(int, ref bool)` (469줄) — `CAXD.AxdoReadOutport` 사용, **구현됨**
- `int ReadDiBit(int, ref bool)` (483줄) — `CAXD.AxdiReadInport`, 구현됨
- `bool ReadDoBit(int)` (496줄) — **미구현 스텁(`return true`)** ← 대상
- `bool ReadDiBit(int)` (501줄) — `CAXD.AxdiReadInport`, 구현됨(실패 시 throw)
- 즉 **DO 읽기 API(`AxdoReadOutport`)는 같은 파일에서 이미 검증**됨(469줄·`GetDoArray` 399줄). bool 반환 오버로드만 DO쪽이 비어 있었음.
## 2. 구현안 (DI 패턴 그대로)
```csharp
public override bool ReadDoBit(int index)
{
uint data = 0;
int err = (int)CAXD.AxdoReadOutport(index, ref data);
if (err != (int)AXT_FUNC_RESULT.AXT_RT_SUCCESS)
throw new Exception(string.Format("ERR_IO_FAIL_TO_GET_BIT;ERROR CODE:{0}", err));
return Convert.ToBoolean(data);
}
```
- `ReadDiBit(int)`(501줄)와 완전 동일 구조, API만 `AxdiReadInport → AxdoReadOutport`.
## 3. 스텁 의존 호출처 분석 (영향 범위)
`bool ReadDoBit(int)` 호출처 전수:
| 호출처 | 위치 | 용도 |
|--------|------|------|
| `UCInformationPanelViewModel.cs` | 46, 53, 82, 115, 132 (5곳) | ModeKeySolOn / SafetyDoorRelease **상태 표시** |
- 5곳 모두 **`ePDOAddress`(PAIX 주소)** 사용 → 실제 **PAIX 보드에서 실행**.
- **PAIX의 `ReadDoBit(int)`는 정상 구현**(PAIXIO.cs:288, `GetDoArray`로 읽음) → 이 호출처는 이미 정상 동작.
- `(int, ref bool)` 오버로드 호출(`UCTestDeviceViewModel.cs:264`)은 Ajin도 구현돼 있어 무관.
- **결론: Ajin 스텁(`return true`)에 실제 의존하는 코드는 사실상 없음.** 영향은 "Ajin 보드 장비에서 위 5개 호출이 도는 경우"로 한정되며, 그 경우 오히려 항상 true가 오표시였음(상태 표시용, 안전제어 아님 → 위험 낮음).
## 4. 현재 결정 상태 (스텁 유지 + 구현 주석 보존)
```csharp
public override bool ReadDoBit(int index)
{
//현재 사용하지 않아서 추후 구현
return true;
// [추후 구현] return true 스텁 의존 코드 확인 후 아래로 교체 (ReadDiBit(int) 패턴)
//uint data = 0;
//int err = (int)CAXD.AxdoReadOutport(index, ref data);
//if (err != (int)AXT_FUNC_RESULT.AXT_RT_SUCCESS)
// throw new Exception(...);
//return Convert.ToBoolean(data);
}
```
- 동작 변화 없음(`return true` 유지) → 현장 안전. 구현은 주석으로 보존해 추후 활성화 용이.
- 빌드 산출물: **AuroraIO.dll**.
## 5. 후속 (활성화 조건)
- Ajin 보드 장비에서 DO 상태 표시 정확성이 필요해질 때, 주석 해제 + 위 5개 호출처 회귀 확인.
- 활성화 시 실패 처리(throw) 영향 검토: `ReadDiBit(int)`와 동일 정책이라 일관성은 확보됨.

View File

@ -1,59 +0,0 @@
# 가이드빔(Guide Beam) 제어 경로 분석 — TruPulse — 2026-06-19
> 계기: 레시피 Guide Beam 버튼/상태가 현장에서 동작하지 않음. IO(DO)→Laser 순으로 추적.
> 결론: 가이드빔은 **Laser(TruPulse) 디바이스 기능**이며, 제어 콜백 **`ExtPilotOnOff`가 미구현(미할당)**.
> 관련: [[110333_alarm_subsystem_analysis]]
---
## 1. 잘못된 경로였던 IO(DO 17) 시도 요약
- 레시피 ViewModel(`UCScannerObjectTeachingControlBoxViewModel.cs`)에서 `_IODevice`(메인 IO_Board=PAIX_PTOP)의
**DO 17(EX_Red_Control)** 을 read/write 하도록 수정 → 상태 표시(읽기)는 동작 확인.
- 그러나 **물리적 가이드빔(적색광)은 출력 안 됨**.
- 현장 확인: 번들(DBW-MC Laser SW)의 `RedLight Opened/Closed` 이벤트는 **번들의 RedClosed 버튼 수동 조작** 결과로,
우리 DO 17과 무관. `Mod1_IO:000100↔000000` 비트만 DO 17과 일치.
- → **DO 17(IO) 경로는 가이드빔 제어가 아님.** (메인 IO_Board와 레이저 가이드빔 입력은 별개)
- 현 상태: 원복하지 않고 **주석 토글로 필요시 전환**(도연님 운용). 기존 `_Laser` 코드는 주석 보존됨.
## 2. 실제 경로 = Laser(TruPulse) 디바이스
레이저 타입 = **TruPulse** (`eLaserType.TruPulse`, `Laser.cs:70`).
```
_Laser.PilotLaserOn() (베이스 Laser, Laser.cs)
└ IsSimul 이면 return E1.OK (no-op) ← 현재 시뮬이라 여기서 멈춤
└ 실모드: TruPulse.PilotLaserOn() (TruPulse.cs:817)
└ return PilotOnOffFunc(true)
└ (Laser.cs:74) core.PilotOnOffFunc = (on) => ExtPilotOnOff(on)
└ ExtPilotOnOff(true) ← ★ 레이저 IO 보드 write 의도 콜백
```
상태 읽기:
```
GetPilotLaserStatus() → PilotLaserEnable ← (TruPulse.cs:174) Di2 & (1<<9)
(QueryStatusHwInterface 로 TruPulse 컨트롤러에서 직접 읽음)
```
## 3. ★ 핵심 문제: `ExtPilotOnOff` 미구현(미할당)
- `Laser.cs` 27~29줄에 `ExtLaserEnable` / `ExtLaserDisable` / `ExtPilotOnOff` **선언만** 존재.
- 전체 소스에 **`ExtPilotOnOff = ...` 할당부 없음**(검색 0건). `updateCore` 구독(`UCTruPulseManualViewModel`)도 core 참조만 받고 콜백 미주입.
- 결과:
- **시뮬(현재)**: 베이스에서 no-op → 빔 안 나옴, 상태 항상 true. (정상)
- **실모드**: `ExtPilotOnOff(true)` 호출 시 **null → NullReferenceException** 위험.
- → "레이저 IO 보드로 가이드빔 제어"는 **설계 의도이나 구현 미완(TODO)**.
## 4. 결론 / TODO
| 구간 | 상태 |
|------|------|
| 가이드빔 제어 의도 | TruPulse → `ExtPilotOnOff` → 레이저 IO 보드 DO |
| 실제 구현 | ❌ 미구현 (`ExtPilotOnOff` 미할당) |
| 현재 운영 | 시뮬 → no-op (물리 빔 안 나오는 게 정상) |
| 레시피 DO 17(`_IODevice`) 변경 | 잘못된 경로(메인 IO_Board), 원복 안 함 — 주석 토글로 운용 |
추후 실제 가이드빔 구현 시:
- `_Laser.ExtPilotOnOff = (on) => { 레이저IO보드._iOUnit.WriteDoBit(<가이드빔 DO>, on); return 0; };` 구현
- "레이저 IO 보드" 디바이스/DO 주소 확정 (메인 PAIX와 별개 여부)
- Laser 디바이스 **비시뮬 + Activated**