수처리 설비 원격 제어 시스템
프로젝트 소개
캡스톤디자인 프로젝트가 끝나갈 때쯤 지인이 내가 만들어둔 PLC 통신 프로그램을 기반으로 실무 프로젝트를 진행해 보자고 하여 시작하게 되었다.
예전부터 꾸준히 PLC 통신을 공부해 왔고 저번에 만든 오므론 온도계 모니터링 및 제어 프로그램이 2월부터 지금까지 문제없이 잘 돌아가는 것을 보고 이번 프로젝트도 잘할 수 있겠다고 생각했다.
프로젝트 내용
구성요소
요소 | 내용 |
PLC | LS산전 XGB-DBCH |
Server | Node.js를 사용하여 구성하였고 크게 3 파트로 나눌 수 있다. PLC와 통신 - TCP기반의 Modbus 통신을 사용하여 데이터 송수신 - PLC주소 변환 Client와 통신 - Socket.io를 사용한 데이터 송수신 - Axios를 사용한 HTTP 비동기 통신 MySQL 데이터베이스 - PLC와 통신으로 받은 데이터를 DB에 저장 - 기간별 데이터 조회 |
Client | React를 사용하여 구성하였고 크게 2 파트로 나눌 수 있다. Server와 통신 - Socket.io를 사용한 데이터 송수신 - Axios를 사용한 HTTP 비동기 통신 UI 업데이트 - Server와 통신으로 받은 데이터를 가공하여 UI에 업데이트 및 렌더링 |
서버
PLC와 통신
https://fortex66.tistory.com/14
지난번에 만든 프로그램과 비슷하지만 이번 프로젝트에서 몇 가지 문제점들을 개선하였다.
통신 방법 개선
기존에는 Client에서 Server로 일정 주기마다 axios 요청을 보내고 Server는 요청을 받으면 Modbus 통신을 통해 PLC에 데이터를 요청하는 방식이었는데 이번에는 Client의 요청과 별개로 Server가 주기마다 PLC로 데이터를 요청하고 받아서 Socket 통신을 통한 Client의 요청이 있다면 Client로 데이터를 송신하는 방식으로 바꾸었다.
Mutex를 사용한 자원관리
async function read_D_Word(register, count) {
const operation = async (client) => {
try {
let data;
status = await modbusConfig.getIsConnected();
if (!status) {
return;
}
// 뮤텍스를 잠금
const release = await modbusConfig.mutex.acquire();
try {
// 공유 자원에 접근하여 레지스터를 읽음
data = await client.readHoldingRegisters(register, count);
} catch (err) {
release(); // 뮤텍스 해제
console.log(err);
console.trace();
await modbusConfig.setIsConnected(false);
} finally {
release(); // 뮤텍스 해제
}
if (data) {
return data.data;
} else {
return [];
}
} catch (err) {
await modbusConfig.setIsConnected(false);
console.error(`Read Holding Registers Error at ${register}`, err);
throw err;
}
};
return await modbusOperation(operation, register, count);
}
기존에는 데이터가 많이 없어서 동시접근 문제가 잘 발생하지 않았지만 이번 프로젝트에서는 데이터량이 많아서 기능별로 나누어서 통신을 하다 보니 통신 주기가 겹치면서 동시접근 문제가 발생하였다. 그래서 Modbus 요청에 Mutex를 사용하여 자원을 관리하였다.
재연결 로직 개선
// Modbus 클라이언트 설정 및 연결
async function connectClient() {
if (client) {
await client.close();
client = null;
}
client = new Modbus();
let attempts = 0;
const maxAttempts = 3;
const retryDelay = 1000;
while (attempts < maxAttempts) {
try {
console.time("modbusConfig.js connectClient while 반복문");
client.setID(1);
client.setTimeout(5000); // 타임아웃 5초
// console.time("client.connectTCP");
await client.connectTCP(HOST, { port: MODBUS_PORT });
// console.timeEnd("client.connectTCP");
if (!client) {
throw new Error("Modbus client not initialized");
}
await setIsConnected(true);
console.timeEnd("modbusConfig.js connectClient while 반복문");
return client;
} catch (error) {
console.log(
`Failed to connect Modbus client on attempt ${attempts + 1}:`,
error
);
console.trace();
attempts++;
if (attempts >= maxAttempts) {
const io = socket.io();
io.emit("modbusError", true);
console.log(
"Max connection attempts reached. Failed to connect Modbus client."
);
console.trace();
client = null;
await setIsConnected(false);
console.timeEnd("modbusConfig.js connectClient while 반복문");
return null;
}
await new Promise((resolve) => setTimeout(resolve, retryDelay)); // 재시도 전 대기 시간
} finally {
console.timeEnd("modbusConfig.js connectClient while 반복문");
}
}
}
네트워크 상태가 좋지 않아 통신이 잘 안 되거나 PLC의 전원이 OFF에서 ON 될 때 기존에도 자동으로 재연결을 했지만 이번 프로젝트에서는 PLC에 연결이 되지 않으면 다른 통신 인터벌들은 다 클리어하고 PLC의 상태를 check 하는 인터벌만 돌아가면서 재연결을 시도하고 3번다 실패하면 Client에 modbusError가 발생했다고 알린다.
PLC 주소 변환기
Modbus 통신을 사용하면 PLC에 모드버스 Bit영역과 Word영역을 설정해야 한다. Server에서 설정한 영역의 Bit영역에 Read, Write 요청을 보내야 하는데 문제는 PLC는 일반적인 16진수와 다르다. (Word영역은 문제가 없다.)
이 부분은 내용이 길어서 아래의 게시글에서 따로 다루었다.
https://fortex66.tistory.com/16
Client와 통신
socket.io를 사용한 데이터 송수신
기존에는 Client에서 Server로 일정 주기마다 axios 요청을 보내고 Server는 요청이 들어오면 PLC Modbus 통신으로 데이터를 받아서 클라이언트로 전달해 주었는데 이번에는 Server가 주기적으로 Modbus 통신으로 데이터를 받아서 socket을 통한 Client의 요청이 있다면 해당하는 데이터를 전달하는 방식으로 통신을 한다. socket을 통한 Client의 요청은 기능별로 나누어서 통신을 하게 만든 것이다.
axios를 사용한 데이터 송수신
로그인, 비밀번호 바꾸기, 데이터베이스 다운로드 등의 단편적인 기능에 사용되었다.
MySQL
데이터베이스는 user, log, alarm으로 구성되어 있으며 sequelize를 사용하여 구성하였다.
클라이언트
Server와 통신
export const SocketContext = createContext();
<SocketContext.Provider value={socket}>
<Router>
<StatusProvider>
<div className="app">
<Routes>
<Route/>
...
<Route/>
</Routes>
</div>
</StatusProvider>
</Router>
</SocketContext.Provider>
SocketContext는 Context API를 사용하여 생성된 컨텍스트이고 이를 통해 socket을 전역적으로 사용할 수 있다.
SocketContext.Provider를 사용하여 socket 객체를 컨텍스트로 전달한다.
const Drive = () => {
const socket = useContext(SocketContext);
const [data, setData] = useState({
dirve_D_part: [],
drive_M_part: [],
});
useEffect(() => {
const handleData = (receivedData) => {
if (
receivedData &&
receivedData.dirve_D_part &&
receivedData.drive_M_part
) {
if (
receivedData.dirve_D_part.length > 0 &&
receivedData.drive_M_part.length > 0
) {
setData({
dirve_D_part: receivedData.dirve_D_part,
drive_M_part: receivedData.drive_M_part,
});
} else {
// console.error(
// "Received data has empty parts, keeping previous state."
// );
}
} else {
// console.error("Received incomplete data, keeping previous state.");
}
};
socket.emit("requestPageData", { page: "drive" }); // Drive 페이지 데이터 요청
socket.on("pageData", handleData);
return () => {
socket.off("pageData", handleData); // 컴포넌트 언마운트 시 이벤트 리스너 제거
};
}, []);
useContext(SocketContext)를 사용하여 socket객체를 컨텍스트에서 가져온다.
useEffect훅을 사용해서 컴포넌트가 마운트 될 때 socket을 통해 서버에 데이터를 요청하고 서버로부터 데이터를 받아서 처리한다.
UI 업데이트
Server와 통신으로 받은 데이터들을 각 페이지에서는 기능에 맞게 가공하여 UI에 뿌려준다.
결과
MAIN 페이지이다.
한눈에 전체 공정이 다 보이도록 구성되어 있으며 모니터링뿐만 아니라 HMI와 똑같이 빠른 제어가 가능하다.
알람 페이지이다.
HMI와 마찬가지로 알람이 발생하면 다른 페이지에 있더라도 자동으로 알람 페이지로 이동하게 만들었다.
실시간 그래프 페이지이다.
기존에 만든 오므론 온도계 프로그램과 비슷하게 만들었고 주기는 1초이다.
데이터베이스 조회 페이지이다.
실시간 그래프 페이지는 담을 수 있는 정보의 양이 적기 때문에 DB에 들어있는 데이터를 조회하여 지나간 데이터들을 한눈에 볼 수 있도록 만들었다.
또한 엑셀파일로 해당 기간의 데이터를 다운로드 가능하고 그래프를 저장하는 기능도 있다.
PLC와의 연결과 네트워크 상태에 따른 재연결로직에 심혈을 기울였기 때문에 결과는 만족스럽게 나온 것 같다.
느낀 점
제법 규모가 있는 실무 프로젝트라서 기간 내에 완성할 수 있을지, 완성하더라도 생각처럼 잘 작동할지 걱정이 많았다.
하지만 오므론 온도계 원격제어 프로그램과 비슷하니까 쉬울 것이라고 생각하고 도전했지만, 규모가 커지면서 미처 간과한 부분이 많이 발생하였고 여러 문제에 부딪혔다.
공모전이나 대외활동 같은 프로젝트와는 전혀 달랐다. 실제로 사용하기 때문에 함수 하나를 만들더라도 좀 더 신경 쓰고 전반적인 시스템을 이해하고 사용자의 관점에서 프로그램을 만들어야 했다.
한 달 정도를 주말 밤낮없이 몰두하면서 문제들을 하나씩 헤쳐 나갔다. 그 과정에서 이론으로만 배웠던 것들을 직접 적용시키면서 많은 것을 배울 수 있었다.
프로젝트를 진행하면서 통신의 중요성을 절실히 깨달았다. 특히, Modbus 통신 부분에서 통신 안정성과 효율성이 시스템 전체의 성능에 얼마나 큰 영향을 미치는지 직접 체감할 수 있었다.