Visual Studio 2008에서 Windows Mobile Device Center(WMDC)는 정상적으로 연결되어 표시되지만, 컴파일이나 디바이스 연결 테스트 시 "RPC 서버를 사용할 수 없습니다" 오류가 발생하는 문제는
주로 Visual Studio의 CoreCon(디버깅 및 통신 에이전트) 서비스가 PC의 DCOM/RPC 통신 권한 문제나 방화벽에 가로막힐 때 발생한다.
문제를 해결하기 위해 다음 단계를 순서대로 진행해 보자.
1.Visual Studio 2008 관리자 권한으로 실행:권한 문제 해결.
바탕화면이나 시작 메뉴에 있는 Visual Studio 2008 아이콘을 우클릭하여 **'관리자 권한으로 실행'**을 선택한다. 그 후 프로젝트를 다시 열고 디바이스 연결을 시도한다.
확인 방법: 연결 테스트나 컴파일 시 "RPC 서버를 사용할 수 없습니다" 오류 대신 정상적으로 빌드가 진행되는지 확인한다.
(난 여기서 해결됨.)
2.Windows 필수 RPC 서비스 상태 확인:서비스 점검.
키보드의 Windows 키 + R을 누르고 services.msc를 입력해 서비스 관리 창을 연다. 목록에서 Remote Procedure Call (RPC), DCOM Server Process Launcher, RPC Endpoint Mapper 서비스가 모두 '실행 중' 상태이고 시작 유형이 **'자동'**으로 설정되어 있는지 확인한다. 만약 중지되어 있다면 우클릭하여 시작한다.
확인 방법: 해당 서비스들의 상태가 모두 '실행 중'으로 표시되는지 확인한다.
3.Windows 방화벽 설정 확인:방화벽 예외 설정.
제어판의 Windows 방화벽 메뉴에서 **'Windows 방화벽을 통해 앱 또는 기능 허용'**으로 이동한다. Visual Studio 디버깅 통신에 필요한 프로세스(ConManClient2.exe, CMAccept.exe 등)나 '파일 및 프린터 공유' 항목이 허용 목록에 체크되어 있는지 확인한다.
확인 방법: 방화벽 설정 변경 후 VS에서 장치와의 통신 시도 시 차단 경고가 뜨지 않는지 확인한다.
4.VS 2008 디바이스 옵션 설정 재확인:연결 방식 점검.
Visual Studio 2008 상단 메뉴에서 [도구] > [옵션] > [장치 도구] > [장치] 순으로 이동한다. 대상 장치가 올바르게 선택되어 있는지 확인하고, **[장치 속성]**을 눌러 연결 방식(ActiveSync 등)이 현재 기기 상태와 일치하는지 점검한다.
확인 방법: 장치 속성 창 내의 '연결 테스트' 버튼을 눌렀을 때 연결 성공 메시지가 출력되는지 확인한다.
우측 솔루션 탐색기에서 설루션이 아닌 C# 프로젝트 이름을 우클릭하고 속성(Properties)을 선택한다.
좌측 메뉴 또는 탭에서 응용 프로그램(Application) 항목을 선택합니다.
리소스(Resources) 또는 아이콘 및 매니페스트(Icon and manifest) 섹션에 있는 아이콘(Icon) 설정 항목을 찾는다.
드롭다운 옆의 찾아보기(Browse) 버튼을 눌러 아까 준비한. ico 파일을 선택한다
저장 및 빌드 확인
설정을 저장(Ctrl + S)한 뒤, 프로젝트를 다시 빌드(Rebuild) .
프로젝트 폴더 내의 bin/Debug 또는 bin/Release 폴더에 들어가 보면 생성된 실행 파일(EXE)의 아이콘이 변경된 것을 확인. (※ 참고: 비주얼 스튜디오에서 디버깅 실행 직후에는 윈도 캐시나 임시 실행 파일 문제로 곧바로 아이콘이 바뀌어 보이지 않을 수 있으나, 탐색기에서 실제 파일을 직접 확인하면 정상적으로 적용되어 있다.)
(II). 폼(Form)의 아이콘 속성에서 변경하기
비주얼 스튜디오 디자이너에서 아이콘을 바꾸고 싶은 해당 폼(. cs [Design])을 연다.
폼을 클릭하여 선택한 뒤, 우측 하단의 속성(Properties) 창을 연다.
속성 목록에서 Icon 항목을 찾는다.
Icon 속성 옆의 값 부분을 누르고 [...] 버튼을 클릭하여, 준비해 둔. ico 파일을 선택.
II-1 코드로 동적 지정하기 (필요한 경우)
만약 코드로 직접 아이콘을 지정하고 싶다면, 폼의 생성자나 Load 이벤트 내부에 다음과 같이 작성.
// 현재 폴더에 있는 icon.ico 파일을 아이콘으로 지정하는 예시
this.Icon = new System.Drawing.Icon("icon.ico");
💡 참고사항
서브 폼들의 아이콘을 따로 지정하지 않으면, 기본적으로 프로젝트 속성에 설정된 메인 아이콘이 그대로 상속된다.
만약 특정 서브 폼만 독특한 아이콘으로 따로 변경하고 싶을 때만 위 방법처럼 개별 폼의 Icon 속성을 따로 지정.
리눅스나 스크립트에 익숙하지 않더라도 라즈베리파이 부팅 시 파이썬 스트림릿(Streamlit) 웹서버가 자동으로 실행되도록 만드는 가장 표준적이고 안전한 방법인 Systemd 서비스 등록 방법을 차근차근 배워 볼까?.
🛠️ 자동 실행 설정 단계 (따라 하기)
1단계: 서비스 설정 파일 만들기
라즈베리파이 터미널 창을 열고 아래 명령어를 입력하여 시스템 서비스 파일을 생성한다. (여기서는 서비스 이름을 streamlit.service로 지정한 다치고.)
sudo nano /etc/systemd/system/streamlit.service
2단계: 아래 내용을 복사해서 붙여 넣기
창이 열리면 아래 내용을 그대로 복사해서 붙여 넣어 준다. (단, 본인의 라즈베리파이 사용자 계정이 pi가 아니거나 스트림릿 파일 경로가 다르다면 User, WorkingDirectory, ExecStart 경로를 본인 환경에 맞게 수정하자.)
Ini, TOML
[Unit]
Description=Streamlit Web Server
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/myApp
ExecStart=/usr/local/bin/streamlit run app.py --server.address=0.0.0.0
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
WorkingDirectory: 스트림릿 소스 코드가 들어있는 폴더 경로
ExecStart: 스트림릿을 실행하는 명령어 경로 (보통 /usr/local/bin/streamlit 또는 python3 -m streamlit)
--server.address=0.0.0.0: 라즈베리파이 내부뿐만 아니라 공유기에 연결된 다른 PC나 스마트폰에서도 웹서버에 접속할 수 있게 해주는 설정이다.
수정 후 저장하는 법: 키보드의 Ctrl + O를 누르고 Enter를 쳐서 저장한 뒤, Ctrl + X를 눌러 편집기를 빠져나온다.
3단계: 시스템에 서비스 등록 및 활성화하기
새로 만든 서비스를 리눅스 시스템에 인식시키고, 부팅 시 자동 실행되도록 설정하는 명령어들을 순서대로 입력한다.
# 1. 시스템에 새 서비스 파일 등록 새로고침
sudo systemctl daemon-reload
# 2. 부팅 시 자동 실행(Enable) 설정
sudo systemctl enable streamlit.service
# 3. 지금 당장 서비스 실행해 보기
sudo systemctl start streamlit.service
4단계: 정상 작동 확인하기
웹서버가 백그라운드에서 오류 없이 잘 돌고 있는지 아래 명령어로 상태를 확인한다.
sudo systemctl status streamlit.service
중간에 초록색 글씨로 Active: active (running) 이 표시되면 성공적으로 실행 중인 것!
나가고 싶을 때는 키보드의 q를 누르면 된다.
이제 라즈베리파이를 재부팅(sudo reboot) 하더라도 전원이 켜지면서 스트림릿 웹서버가 자동으로 백그라운드에서 실행될 거다.
앞서 설명한 내용 중 Ini, TOML은 뭘까?
프로그램이나 시스템의 환경 설정 파일(Configuration File)의 포맷(형식)을 뜻한다.
파이썬이나 라즈베리파이에서 프로그램을 만들다 보면 서버 주소, 포트 번호, 비밀번호, 화면 위치 같은 다양한 설정값을 코드 외부에 따로 저장해 두어야 할 때가 많다. 이때 주로 사용하는 대표적인 설정 파일 형식들입니다.
📝 설정 파일 포맷이란?
INI 형식 (.ini)
윈도우 프로그램 등에서 아주 오래전부터 쓰여온 전통적인 방식.
대괄호로 카테고리를 나누고 키=값 형태로 간단하게 작성한다.
예시:
Ini, TOML
[Server]
ip = 192.168.1.10
port = 502
TOML 형식 (.toml)
최근 파이썬 진영(특히 스트림릿이나 패키지 관리 도구 등)에서 설정 파일로 매우 선호하는 최신 포맷.
사람이 읽기 매우 쉽고 구조화된 데이터(주석, 숫자, 리스트 등)를 깔끔하게 표현할 수 있다.
예시:
Ini, TOML
[server]
address = "0.0.0.0"
port = 8501
enableCORS = false
라즈베리파이에서 스트림릿(Streamlit) 같은 웹서버나 파이썬 프로그램을 실행할 때, 설정을 코드에 직접 적어두지 않고 이런 INI나 TOML 같은 설정 파일에 따로 적어둔 뒤 불러와서 사용하면 나중에 설정을 바꿀 때 코드를 고칠 필요 없이 파일만 수정하면 되기 때문에 매우 편리할걸!
패키지 이름: NModbus (또는 최신 .NET 환경에서 호환되는 NModbus 포크 버전) (또는 패키지 관리자 콘솔에서 Install-Package NModbus 입력)
설치화면 I.
설치화면 II.
Install-Package NModbus
설치확인화면 III.
사용예:
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NModbus;
string serverIp = "172.16.32.108"; // 대상 장치 IP
int port = 502;
CancellationTokenSource cts = new CancellationTokenSource();
Console.WriteLine("[시스템] Modbus TCP 자동 재접속 클라이언트를 시작...");
// 메인 루프: 연결이 끊기면 무한으로 재접속 시도
while (!cts.Token.IsCancellationRequested)
{
TcpClient client = null;
IModbusMaster master = null; // 수정됨: IModbusIpMaster -> IModbusMaster
try
{
// 1. 서버 연결 시도
client = new TcpClient();
Console.WriteLine($"[연결 시도] {serverIp}:{port} 연결 중...");
// 연결 타임아웃 설정 (3초 안에 응답 없으면 예외 발생)
var connectTask = client.ConnectAsync(serverIp, port);
if (await Task.WhenAny(connectTask, Task.Delay(3000)) != connectTask)
{
throw new TimeoutException("연결 시간 초과");
}
Console.WriteLine("[연결 성공] 통신을 시작.");
// 2. NModbus 3.x 버전에 맞는 Master 생성 방식
var factory = new ModbusFactory();
master = factory.CreateMaster(client); // 수정됨: CreateIpMaster -> CreateMaster
// 3. 수신(폴링)을 담당하는 백그라운드 스레드 실행
await RunCommunicationLoop(master, cts.Token);
}
catch (Exception ex)
{
Console.WriteLine($"[통신 오류 또는 연결 끊김] {ex.Message}");
}
finally
{
// 사용한 자원(소켓 등) 깨끗이 정리
master?.Dispose();
client?.Dispose();
Console.WriteLine("[자원 해제] 연결을 닫고 재접속을 준비.");
}
// 4. 바로 재시도하면 서버에 부하를 주므로 3초 대기 후 재접속 시도
if (!cts.Token.IsCancellationRequested)
{
Console.WriteLine("[재접속 대기] 3초 후 다시 연결을 시도...");
await Task.Delay(3000, cts.Token);
}
}
// ==========================================
// 통신 및 데이터 수신 루프 메서드
// ==========================================
async Task RunCommunicationLoop(IModbusMaster master, CancellationToken cancellationToken) // 수정됨
{
byte slaveId = 1;
ushort startAddress = 0;
ushort numRegisters = 10;
while (!cancellationToken.IsCancellationRequested)
{
// 데이터 읽기 (Function 3 예시)
ushort[] registers = await master.ReadHoldingRegistersAsync(slaveId, startAddress, numRegisters);
Console.WriteLine($"[수신 데이터] Reg[0]: {registers[0]}, Reg[1]: {registers[1]}");
// 주기 조절 (1초 간격)
await Task.Delay(1000, cancellationToken);
}
}
라이브러리를 사용할지, 아니면 기존처럼 C# 기본 TcpClient로 직접 소켓 통신을 구현할지 고민.
두 방식의 장단점을 명확하게 비교
1. NModbus 라이브러리를 사용할 때
장점:
생산성 최고: Modbus 프로토콜은 바이트 단위의 프레임(Function Code, 주소, 데이터, CRC 체크섬 등)을 엄격하게 지켜야 하는데, 라이브러리가 이 복잡한 과정을 전부 알아서 처리해 줌 (master.ReadHoldingRegistersAsync 한 줄이면 끝난다.)
오류 방지: 통신 과정에서 발생할 수 있는 프로토콜 규칙 오류나 CRC 에러 계산 등을 직접 구현할 필요가 없어 개발 시간이 획기적으로 줄어든다.
단점:
혹시 겪을수 있는 라이브러리 버전(3.x)에 따라 네임스페이스나 메서드 이름이 바뀔 때 문서를 찾아봐야 하는 약간의 러닝커브가 있다.
2. 직접 TcpClient로 구현할 때
장점:
외부 의존성 제로: NuGet 패키지를 쓸 필요가 없으므로 버전 호환성 문제로 골치 아플 일이 전혀 없다.
완벽한 제어: Modbus TCP 패킷(MBAP Header + PDU) 구조를 직접 바이트 배열(byte [])로 조립하고 파싱 하므로, 프로토콜 구조를 뼛속까지 이해할 수 있다.
단점:
구현의 번거로움: 소켓으로 바이트를 보낸 뒤, 응답 데이터의 바이트 배열을 받아와서 Function Code가 에러인지 정상인지, CRC나 트랜잭션 ID는 맞는지 직접 쪼개고 검증하는 코드를 일일이 다 짜야 한다. (생각보다 손이 많이 가고 실수가 발생하기 쉽다.)
💡 최종 추천
안전하고 빠른 개발을 원한다면 👉 NModbus 라이브러리 사용을 추천.
비록 처음에 설정과 버전 차이 때문에 약간의 시행착오가 있었지만, 일단 한번 세팅해 두면 이후부터는 센서나 PLC 데이터를 읽고 쓰는 비즈니스 로직(핵심 기능) 개발에만 집중할 수 있어 훨씬 효율적.
만약 프로토콜을 직접 바이트 단위로 제어하는 재미를 느끼고 싶거나 외부 라이브러리 자체를 배제하고 싶다면 👉 직접 구현도 나쁘지 않지만, 산업용 현장에서 안정성과 유지보수를 고려한다면 검증된 라이브러리를 쓰는 것이 실무 표준이래나 뭐래나.
이미 NModbus 패키지도 설치했고 기본 구조를 잡아가고 있으니, 이번 기회에 라이브러리를 활용해 빠르게 기능을 완성해 볼가 한다 나는~~
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NModbus;
class Program
{
static async Task Main(string[] args)
{
// 1. 설정 정보 (라즈베리파이에서 접속할 대상 장치 IP와 포트)
string serverIp = "192.168.0.4"; // 대상 장치 IP (필요시 변경)
int port = 502; // Modbus TCP 기본 포트
CancellationTokenSource cts = new CancellationTokenSource();
// 콘솔 종료 시 안전하게 취소하기 위한 이벤트 처리 (Ctrl + C)
Console.CancelKeyPress += (sender, e) =>
{
e.Cancel = true;
cts.Cancel();
Console.WriteLine("\n[시스템] 종료 요청을 받았습니다. 자원을 정리합니다...");
};
Console.WriteLine("[시스템] 라즈베리파이 Modbus TCP 콘솔 클라이언트를 시작합니다...");
Console.WriteLine($"[대상 정보] IP: {serverIp}, Port: {port}");
Console.WriteLine("------------------------------------------------------------");
// 메인 루프: 연결이 끊기면 3초 후 무한 재접속 시도
while (!cts.Token.IsCancellationRequested)
{
TcpClient client = null;
IModbusMaster master = null;
try
{
// 2. TCP 서버 연결 시도 (3초 타임아웃)
client = new TcpClient();
Console.WriteLine($"\n[연결 시도] {serverIp}:{port} 연결 중...");
var connectTask = client.ConnectAsync(serverIp, port);
if (await Task.WhenAny(connectTask, Task.Delay(3000, cts.Token)) != connectTask)
{
throw new TimeoutException("연결 시간 초과 (3초)");
}
Console.WriteLine("[연결 성공] Modbus 통신을 시작합니다. (1초 주기 요청)");
// 3. NModbus Master 생성
var factory = new ModbusFactory();
master = factory.CreateMaster(client);
byte slaveId = 1; // 장비 ID (Unit ID)
ushort startAddress = 0x0004; // 시작 주소 (0x04)
ushort numRegisters = 0x59; // 요청 개수 (0x59 = 89개)
// 4. 1초 간격 데이터 요청 및 디스플레이 루프
while (!cts.Token.IsCancellationRequested)
{
try
{
// Function Code 4: Read Input Registers
ushort[] registers = await master.ReadInputRegistersAsync(slaveId, startAddress, numRegisters);
// 수신된 데이터를 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]}");
}
}
catch (Exception ex)
{
Console.WriteLine($" ➔ [통신 에러 발생]: {ex.Message}");
break; // 통신 에러 발생 시 내부 루프를 탈출하여 재접속 수행
}
// 1초 대기
await Task.Delay(1000, cts.Token);
}
}
catch (Exception ex)
{
Console.WriteLine($"[연결 오류]: {ex.Message}");
}
finally
{
// 자원 정리
master?.Dispose();
client?.Dispose();
Console.WriteLine("[자원 해제] 연결을 닫았습니다. 3초 후 재접속을 시도합니다.");
}
// 재접속 대기 (취소 요청이 없을 경우)
if (!cts.Token.IsCancellationRequested)
{
try
{
await Task.Delay(3000, cts.Token);
}
catch (TaskCanceledException)
{
break;
}
}
}
Console.WriteLine("[시스템] 프로그램이 완전히 종료되었습니다.");
}
}