CModbusClientTcp.cs
using NModbus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace ENS_RaspberryPi
{
/// <summary>
/// 윈도우용 CTcpModbusClient 역할을 대체하는 라즈베리파이용 Modbus TCP 워커 클래스
/// </summary>
class CModbusClientTcp
{
private readonly string _deviceName;
private readonly string _ipAddress;
private readonly int _port;
private readonly int _intervalMs;
public CModbusClientTcp(string deviceName, string ipAddress, int port, int intervalMs)
{
_deviceName = deviceName;
_ipAddress = ipAddress;
_port = port;
_intervalMs = intervalMs;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
//취소 신호가 들어오기 전까지 계속해서 통신을 시도하고 유지하는 큰 틀의 while 반복문.
while (!cancellationToken.IsCancellationRequested)
{
TcpClient client = null;
IModbusMaster master = null;
try
{
// TCP 연결 준비: 소켓 통신을 위한 TcpClient를 만들고, 지정한 IP와 포트로 비동기 연결을 시도.
Console.WriteLine($"\n[{_deviceName}] 연결 시도중... ({_ipAddress}:{_port})");
client = new TcpClient();
var connectTask = client.ConnectAsync(_ipAddress, _port);
// 3초 안에 연결되지 않으면 타임아웃 예외 발생 :
// 장치가 꺼져있거나 네트워크가 끊겨서 무한 대기에 빠지는 걸 막기 위해,
// "딱 3초만 기다려보고 연결 안 되면 에러를 발생시키고(TimeoutException)" 타이머를 건다.
if (await Task.WhenAny(connectTask, Task.Delay(3000, cancellationToken)) != connectTask)
{
throw new TimeoutException("TCP 연결 시간 초과");
}
// 모드버스 마스터 생성:
// TCP 연결이 성공하면 NModbus 라이브러리의 ModbusFactory를 이용해 통신 master를 만든다.
// 그리고 슬레이브 ID는 1, 읽기 시작할 주소는 0x0004, 개수는 0x59(89개)로 정의합니다.
Console.WriteLine($"[{_deviceName}] 연결 성공! 1초 주기 데이터 요청 시작.");
var factory = new ModbusFactory();
master = factory.CreateMaster(client);
byte unitId = 1;
ushort startAddress = 0x0004; // 윈도우 코드에서 사용한 시작 주소 예시
ushort registerCount = 0x59; // 89개 레지스터 요청
// 순차적 단일 스레드 비동기 루프 (교착 상태 없음) 데이터 반복 수신 루프:
// 1.master.ReadInputRegistersAsync: Func 4번을 이용해 장치로부터 입력 레지스터 데이터를 싹 긁어옴.
// 2.DisplayReceivedData: 받아온 데이터를 화면에 출력.
// 3.통신 도중 에러가 나면 break로 안쪽 루프를 탈출해 재접속 단계로 넘어감.
// 4.await Task.Delay: 설정한 주기(여기서는 0.2초)만큼 쉬었다가 다시 데이터를 요청한다.
// [수정 포인트 1] 내부 루프에서 복잡한 통신 로직을 별도 메서드로 분리하여 깔끔하게 호출
while (!cancellationToken.IsCancellationRequested)
{
bool isSuccess = await ReadAndDisplayDataAsync(master, unitId, startAddress, registerCount, cancellationToken);
// 통신 에러가 발생했다면(false 리턴) 내부 루프를 탈출하여 재접속 단계로 이동
if (!isSuccess)
{
break;
}
// 설정된 간격 만큼 대기
await Task.Delay(_intervalMs, cancellationToken);
}
}
// 통신이 끊기거나 에러가 나서 루프를 빠져나오면
catch (Exception ex)
{
Console.WriteLine($"[{_deviceName}] 연결/통신 오류: {ex.Message}");
}
finally // 자원 정리 및 재시도
{
//열어뒀던 통신망을 정리(Dispose) 메모리 누수막음
master?.Dispose();
client?.Dispose();
Console.WriteLine($"[{_deviceName}] 연결 해제. 3초 후 재접속을 시도합니다.");
}
// 3초 뒤에 다시 처음부터 while 문을 돌며 자동 재접속을 시도.
// 재접속 전 3초 대기
if (!cancellationToken.IsCancellationRequested)
{
try
{
await Task.Delay(3000, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
}
}
private async Task<bool> ReadAndDisplayDataAsync(IModbusMaster master, byte unitId, ushort startAddress, ushort registerCount, CancellationToken cancellationToken)
{
try
{
// Function Code 4 (Read Input Registers) - 취소 토큰을 함께 전달하여 안전성 확보
ushort[] registers = await master.ReadInputRegistersAsync(unitId, startAddress, registerCount);
// 수신 데이터 디스플레이 출력
DisplayReceivedData(registers);
return true; // 성공 시 true 반환
}
catch (Exception ex)
{
Console.WriteLine($"[{_deviceName}] 통신 에러 발생: {ex.Message}");
return false; // 에러 발생 시 false를 반환하여 바깥쪽 루프가 재접속을 하도록 유도
}
}
private void DisplayReceivedData(ushort[] registers)
{
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
Console.WriteLine($"[{timestamp}] [{_deviceName}] 수신 완료 (총 {registers.Length}개 레지스터)");
// 수신된 데이터를 1궤도(Hex 형식)로 변환
string hexValues = string.Join(", ", Array.ConvertAll(registers, r => $"0x{r:X4}"));
// 화면 출력 (디스플레이)
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] ➔ [수신 성공] 총 {registers.Length}개 레지스터 읽기 완료");
Console.WriteLine($" Hex 데이터: {hexValues}");
// 예시로 첫 번째 레지스터 값(Reg[0])의 십진수 값도 함께 출력
if (registers.Length > 0)
{
Console.WriteLine($" ➔ Reg[0] Dec: {registers[0]}, Hex: 0x{registers[0]:X4}");
}
}
}
}
Program.cs
/*
* 모듈이름 :
* 작성일자 : 2026. 08. 25
* 작성자명 : Hwang KyuSeok
*
*
* **/
using ENS_RaspberryPi; // CModbusClientTcp 클래스 사용
using NModbus; // NModbus 라이브러리 사용
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
Console.WriteLine("[시스템] 라즈베리파이 Modbus TCP 클라이언트 시작...");
// 윈도우용 소스의 Cfg_Tcp 설정값들을 빗대어 객체 생성
// (예: 설비용 PCS 또는 피시험품 PCS 접속 정보)
var modbusWorker = new CModbusClientTcp(
deviceName: "PCS_Equipment",
ipAddress: "192.168.0.4",
port: 502,
intervalMs: 200
);
// 취소 토큰 소스(버튼장치) 생성 : 언제든지 취소 요청 가능
CancellationTokenSource cts = new CancellationTokenSource();
// Ctrl + C 입력 시 안전하게 종료 :
// Ctrl + C를 누를 경우, 프로그램을 강제로 튕기지 않고 안전하게 멈추기 위해
// 위에서 만든 취소 리모컨(cts.Cancel())을 누르도록 이벤트를 등록.
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
cts.Cancel();
Console.WriteLine("\n[시스템] 종료 중...");
};
// 백그라운드에서 주기적 통신 시작:
// 앞에서 만든 통신 로봇에게 '지금부터 비동기(StartAsync)로 통신을 시작해 줘!' 하고 명령을 내림.
// 프로그램이 멈추지 않고 계속 데이터를 주고받도록 백그라운드에서 실행된다.
var workTask = modbusWorker.StartAsync(cts.Token);
// 프로그램이 종료되지 않도록 대기 :
// 프로그램이 사용자나 시스템에 의해 종료될 때까지 안전하게 기다리다,
// 완전히 끝나면 마지막 문구를 띄우고 프로그램을 종료다.
try
{
await workTask;
}
catch (TaskCanceledException)
{
// 정상 취소
}
Console.WriteLine("[시스템] 프로그램이 종료되었습니다.");