퇴근5분전

 

  플젝 끝내고 지난 일주일간 코파일럿, 제미나이로 검색을 통해 로컬AI로 할 수 있는게 뭘까를 

계속 찾아봤다.  여러가지 케이스를 정의하고 써먹을 곳을 찾았고...

 

 오늘 그 시작을 했다. 

노트북[ 올라마 설치, llama3.2설치] :: 외장그래픽카드 4060 ( 8GB )

데스크탑PC[VS에서 윈폼프로젝트(Net 8.0) 생성, 시멘틱커널 사용하는 누겟 설치 ]

그리고 테스트 코드를 두개 만들었다. 

 

1. "A:10, B:20, C30에서 15보다 큰 것을 찾아줘" 이런 프롬프트로 올라마에 던져서 

데스크탑과 노트북에서 데이타 주고 받고 잘 하는지 봤고,

 

2. 시멘틱커널에 플러그인을 생성해서 두 숫자를 더하는 메소드가 요청 처리에 호출되는지 확인했다. 

 

위 두개 모두 성공했다. 

 

 로컬AI인 만큼 로컬에서 데이타를 제공하고 AI가 나머지 처리를 하도록 프롬프트를 작성 잘 하는건데...

이때 필요한 데이타는 플러그인이나 프롬프트에 추가해서 제공하는 것으로 그 방향성을 잡았다. 

 

이렇게 하면 할 수 있는 일들이 많다. 반복적인 일들을 플러그인을 추가하면서 AI에 맡길 수 있고.

네트웍 내부 망에서 데이타를 가져다 AI에 제공도 가능하고... VRAM이 커야 좀 더 큰 모델을 쓸 수 있겠지만... 

그거야 나중에 그래픽카드를 좋은거 쓰든가.. ai용 머신을 사든가... 등등... 

 

 일단 생각하는데로 ai를 굴릴수 있는지 확인하는 것이 먼저라서...

마지막 프로젝트가 설비관련 MES의 DAT였으니까... 그와 관련된 몇가지 케이스를 적어놨는데... 

흉내만 내볼 예정이다. 

 

 위 간단한 10 + 30을 플러그인으로 처리 요청을 보냈더니 5~7초 걸리더라... 

한번 구성해두면 매번 같은 일 안해도 될 것 같은 생각에 구조에 신경써보자. 

 

 

 

 

 

 

 일단 프로젝트가 저번주에 끝났다. 

 

Tcp/Ip 통신으로 PLC와 통신하여 값들을 읽어서 설비동작에 응답하고, DB저장하는 처리를 하였는데... 

 

랜선 뽑기.. 대응이 잘 안되더라.

 

설비에서 진동? 같은걸로 통신중에 랜선이 망가지거나... PLC가 동작을 안해서라든가... 통신카드가 고장났다던가... 

라는 상황에 대비해 랜선을 뽑아보는 테스트를 했는데...

반응이 느리다.. 20초 걸리고 스탑되니까... 

 

 월화 이틀간 여러 테스트를 직접 해보고 찾아봤는데... tcpip는 좀 다르긴 하다. 

보통 뭔가 사용하기전 체크를 먼저 다 마치고 사용을 하는 구조로 작업을 하는데... 

tcpip는 우선 던지고 예외가 발생하면 끊김여부를 확인 할 수 있어서... 

이런 식으로 코드를 변경해서 샘플 테스트를 마쳤다. 새로 만든 통신 코드는 잘 되는데, 다른 문제가 생겼네? 

 

C#을 주력언어로 사용하는 나는 옛날꼰날부터 비동기 코드하면

대리자.BeginInvoke... 를 이용해서 비동기 코드를 작성해왔다. 

PLC 프로그램 하면서 Task를 써보기로 하고, Task.Run 을 이용해서 비동기 코드를 작성했었고.. 

async, await는 그렇게 자주 사용하는 코드는 아니었는데... 

이번 일 끝나고 좀 더 깊이 공부를 했다. 이유는 LS 산전 PLC 통신 프로토콜을 이용해서 통신하는데 

기존 구현되어 있던 코드가 이상괴팍해서리... 새로 만들어줄까 싶어서 조금 변경했더니... 

 패킷이 꼬이더라... 이유는 PLC에 주소값 요청을 날리고 바로 수신하면 그 응답이 오는 구조인데...

백그라운드 워커가 25개정도 돌아가는데 각 워커에서 요청 응답을 날리니까 이게 하나만 꼬여도

응답이 제대로 안넘어오고 섞이는게 생기더라. ( 희안한 경험이었다. )

 그래서 LS 통신 프로토콜 스팩이랑 코파일럿 AI등으로 검색등을 해보니.. 패킷중에 InvokeID라는 필드값이 있었다. 

요청에 InvokeID를 보내면 응답으로 InokeID가 오는 것으로 이것으로 구분이 가능했다. 

 이거 저거 체크하면서 구조를 콜백구조로 바꾸고 InvokeID로 대응시켜서 처리하다보니 Task를 써서 잘 만들어보고 싶어서 ... 

Task를 좀 더 봤다. 

 

async Task MethodName( )

{

         await Task.Delay(100);

}

 이렇게 작성된 것 차체만으로도 비동기적으로 돌아가는걸 이제 알았다. 헐!...

 새로 만든다면 이런 구조가 필요한 곳들이 많았는데... Task.Run안쓰고 위 구조로 하면 굳이 쓰레드를 따로 할당받고 복귀하고 할 필요가 없다는 것도 AI의 답변이고... 

 

 게다가 위처럼 만들면 UI 컨트롤에 값 설정할때 굳이 Invoke 안해도 되고... 

또 하나 공부를 마쳤다. 새로 만드는 기회가 있다면 좀 더 코드가 정리 될 것 같다. 

 

 

 

 

설비에서 로그파일을 떨구면 

그 로그파일을 분석해서 데이타를 DB로 전송하는 코드 일부이다. 

 

- INI

마지막 수정일시.

파일내 읽은 위치값.

 

- 코드 블럭이 좀 이상하게 나오네??

 

 

 

string logFileName = Path.Combine(AppConfig.AM029_LOG_FILE_DIR, $"{state.LastWorkDate}.log");
//Write($"파일:{logFileName}  ☆");
if (File.Exists(logFileName))
{
    // 파일 수정 일시를 체크하여...
    // 수정하지 않았으면 읽지 않음.
    FileInfo f = new FileInfo(logFileName);
    if (INI.LastWriteTime.HasValue && f.LastWriteTime <= INI.LastWriteTime) return; 
    INI.LastWriteTimeString = $"{f.LastWriteTime:yyyy-MM-dd HH:mm:ss}";
    
    using (var fs = new FileStream(logFileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
    {
        fs.Seek(state.LastPosition, SeekOrigin.Begin);
        using (var sr = new StreamReader(fs))
        {
              string line = null;
              while ((line = await sr.ReadLineAsync()) != null)
              {
              	    TY_PARSE = "00";
                    TY_PARSE = line.Contains("SearchSku") ? "01" : TY_PARSE;
                    TY_PARSE = line.Contains("H:SerialNo") ? "02" : TY_PARSE;
                    ... 텍스트 라인별로 키워드로 문자열로 파서구분자 셋팅 ...                   
                    
                    
                    
                    // 셋팅된 구분자로 처리
              	    switch (TY_PARSE)
                    {
                    	default:
                    	case "00": 
                        	break;
                        case "01":
                        	START_TIME = GetDate(line);
                        	파싱....
                        	break;
                    
                    	case "99":
                        
                        	INI.LastWorkDate = state.LastWorkDate;
                            INI.LastPosition = state.LastPosition;
                            
                            Save(...);                            
                        	break;
                    
              	    }                    
                    
              	    if (state.LastWorkDateTime.HasValue)
                    {
                        // 새 파일이 생성되었다면.
                        // 현재 파일은 끝난거니까...
                        string[] files = System.IO.Directory.GetFiles(AppConfig.AM029_LOG_FILE_DIR, "*.log");                                             
                        var fslst = files.Where(file => $"{state.LastWorkDateTime.Value:yyyyMMdd}.log".CompareTo(System.IO.Path.GetFileName(file)) < 0)
                                         .Select(file => System.IO.Path.GetFileName(file));
                        if (fslst.Count() <= 0)
                        { 
                        	return;
                        }
                        string nextLogFileName = fslst.First();                                            
                        string nextLogFilePath = Path.Combine(AppConfig.AM029_LOG_FILE_DIR, nextLogFileName);                                        
                        if (File.Exists(nextLogFilePath))
                        {
                            //이전 파일과 새 파일을 이어줘야 하는데... 
                            state.LastWorkDate = $"{nextLogFileName.Substring(0, nextLogFileName.Length - ".log".Length)}";
                            state.LastPosition = 0;
                            Write($"파일 교체 : {nextLogFilePath} ☆");
                            return;
                         }                                            
                    }              
              }
        }
    }
}


internal static string GetDate(string line)
{
    //[01:55:57.698] 	SearchSku	Test Spec ...
    string timeString = "";
    string find = "]";
    int idx = line.IndexOf(find)+ find.Length;
    if (0 < idx && idx < line.Length)
    {
    	timeString = line.Substring(0, idx);
    }
    return timeString.Trim('[', ']', ' ');
}