[Node.js] Serialport 구현하기
![[Node.js] Serialport 구현하기](/content/images/size/w1920/2024/12/nodejs-serialport.png)
Node.js에서 Serial 통신 구현하기 ⚙️✨
1. Serial 통신이란? 🤔
Serial 통신은 데이터를 비트(bit) 단위로 순차적으로 전송하는 통신 방식이다. 일반적으로 하드웨어 장치(예: 센서, 모뎀, GPS 모듈 등)와 소프트웨어 간 데이터를 주고받을 때 사용된다.
Node.js에서는 serialport
라이브러리를 사용하여 Serial 통신을 구현할 수 있다. 이 라이브러리는 직렬 포트를 제어하고 데이터를 송수신하기 위한 강력한 도구를 제공한다.
2. serialport
설치하기 🛠️
먼저 serialport
라이브러리를 설치해야 한다.
npm install serialport
3. Serial 통신 기본 구성 🚀
1) Serial 포트 열기와 닫기
Serial 포트를 열고 닫는 것은 장치와의 연결을 설정하고 해제하는 기본 작업이다.
아래 코드는 Serial 포트를 여는 openPort
와 닫는 closePort
함수를 구현한 예제이다.
import { SerialPort } from 'serialport';
const port = new SerialPort({
path: 'COM3', // 포트 경로
baudRate: 115200, // 통신 속도
autoOpen: false, // 자동으로 포트를 열지 않음
});
// Serial 포트 열기
export const openPort = async () => {
return new Promise((resolve, reject) => {
port.open((err) => {
if (err) {
console.error(`Failed to open serial port: ${err.message}`);
reject(err);
} else {
console.log('Serial port is open');
resolve();
}
});
});
};
// Serial 포트 닫기
export const closePort = async () => {
return new Promise((resolve, reject) => {
port.close((err) => {
if (err) {
console.error(`Failed to close serial port: ${err.message}`);
reject(err);
} else {
console.log('Serial port is closed');
resolve();
}
});
});
};
2) 데이터 송수신 구현
시리얼 데이터를 송신(write)하고 수신(read)하는 작업은 Serial 통신의 핵심이다.
데이터 수신:
데이터를 수신하려면 이벤트 리스너를 통해 처리할 수 있다.
port.on('data', (data) => {
console.log(`Received data: ${data}`);
});
데이터 송신:
export const writePort = async (command) => {
return new Promise((resolve, reject) => {
port.write(command, (err) => {
if (err) {
console.error(`Error writing to serial port: ${err.message}`);
reject(err);
} else {
console.log(`Data written: ${command}`);
resolve();
}
});
});
};
4. 실무 예제: SMS 서비스 구현하기 📡
실무에서 개발하여 사용중인 코드를 활용해 Serial 통신을 통한 SMS 서비스를 구현하는 과정을 살펴보자.
1) Serial 통신 모듈 (serial_module.js
) ⚙️
이 모듈은 Serial 포트를 열고 닫으며, 데이터를 송수신하고 리스너를 관리하는 역할을 한다.
주요 기능:
- Serial 포트 열기와 닫기 (
openPort
,closePort
) - RX 데이터 변환(
parser
) - 데이터 송수신 (
writePort
) - 누적 데이터를 가져오기 및 초기화 (
getCollectedDataAndClear
) - 데이터 및 에러 리스너 관리 (
registerListeners
,removeAllListeners
)
1-1) Serial Open, Close, Listener 구현
import { DelimiterParser, SerialPort } from 'serialport';
import { logger } from '#util/logger.js';
import { config } from '#config/config.js';
import { sleep } from '#util/wb_lib.js';
const port = new SerialPort({
path: config.serial.port,
baudRate: config.serial.baudRate,
autoOpen: false,
});
// 연속적인 바이트 흐름인 시리얼 데이터를 캐리지리턴 기준으로 한 줄로 처리하여 보기 편하게 변환
const parser = port.pipe(new DelimiterParser({ delimiter: Buffer.from('\r\n') }));
// 데이터를 수집하기 위한 변수
let collectedData = '';
// serial port open
export const openPort = async () => {
await sleep(500);
if (port.isOpen) {
logger.info(`[openPort] Serial port is already OPEN`);
return;
}
return new Promise((resolve, reject) => {
port.open((err) => {
if (err) {
logger.error(`[openPort] Failed to open serial port: ${err.message}`);
reject(new Error(`Serial Port Open Error: ${err.message}`));
} else {
logger.info(`[openPort] Serial port is OPEN`);
registerListeners(); // 포트를 열 때 리스너 등록
resolve();
}
});
});
};
// serial port close
export const closePort = async () => {
await sleep(500);
if (!port.isOpen) {
logger.info(`[closePort] Serial port is already CLOSED`);
return;
}
return new Promise((resolve, reject) => {
port.close((err) => {
if (err) {
logger.error(`[closePort] Failed to close serial port: ${err.message}`);
reject(new Error(`Serial Port Close Error: ${err.message}`));
} else {
logger.info(`[closePort] Serial Port is CLOSED`);
removeAllListeners(); // 포트를 닫을 때만 리스너 해제
resolve();
}
});
});
};
// 리스너를 등록
const registerListeners = () => {
if (!parser.listenerCount('data')) {
parser.on('data', onData);
}
if (!parser.listenerCount('error')) {
parser.on('error', onError);
}
};
// 리스너 해제
const removeAllListeners = () => {
logger.info(`[removeAllListeners] Removing all parser listeners`);
parser.removeAllListeners('data');
parser.removeAllListeners('error');
};
코드 설명:
autoOpen
: 시리얼 통신은 소스에서 직접 열기, 닫기를 제어 할 것이므로autoOpen
은false
로 설정한다. (default값: true)- 원할한 동작을 위하여
sleep
함수를 별도로 구현하여 500ms정도 딜레이를 준다.(거의 필수..) port.isOpen
: 서비스단에서 시리얼 통신 초기화를 진행해야하는데, 이미 열려있거나 닫혀있는 상태에서 open, close를 하면 애러가 발생하므로 예외처리를 추가한다.registerListeners()
: Port를 Open할때 리스너도 생성을 해서 Data를 받을 수 있도록 준비한다.removeAllListeners()
: Port를 Close할때 리스너를 해제하여 누적되어있던 Data를 초기화 한다.parser
: 시리얼 데이터는 string 형식으로 쭉 데이터가 통신하는 방식이 아니라 bit단위로 순차적으로 전송하는 통신방식이므로 로우데이터를 받으면 사용자는 데이터를 확인하는데에 어려움이 있다. 이를 쉽게 보기위하여 parser 기능을 사용한다.
1-2) Serial Write, Receive 구현
// serial data write
export const writePort = async (command) => {
await sleep(500);
return new Promise((resolve, reject) => {
port.write(command, (err) => {
sleep(500);
if (err) {
logger.error(`[writePort] Error writing to serial port: ${err.message}`);
reject(new Error(`Serial Port Write Error: ${err.message}`));
} else {
logger.info(`[writePort] Success Serial to write data: ${command}`);
resolve();
}
});
});
};
// 데이터를 수신하면서 누적하는 리스너
const onData = (data) => {
const response = data.toString();
collectedData += response + '\n'; // 데이터를 누적
logger.info(`RX: ${response}`);
};
// 에러 리스너
const onError = (err) => {
logger.error(`[parserError] Serial port error occurred: ${err.message}`);
return Promise.reject(new Error(`Serial Port Response Error: ${err.message}`));
};
// 누적된 데이터를 호출하는 함수 (호출 후 초기화)
export const getCollectedDataAndClear = async () => {
await sleep(1000); // 대기 시간을 두어 수신이 완료될 시간을 줌
const data = collectedData;
collectedData = ''; // 데이터 초기화
logger.info(`[getCollectedDataAndClear] Collected data: \n${data}`);
return data;
};
코드 설명:
onData(data)
: 시리얼 데이터는 한번에 리시브 받을수도있고, 여러번 거쳐서 리시블르 받을수도 있다. 그래서 리시브를 받을 때 마다 변수에 차곡차곡 저장을 해놓고 각 리시브 마다 캐리지리턴을 한다.onError(err)
: 리시브를 받을 때 에러처리를 구현한다. 에러처리는 모든 소스에서 제일 중요하다getCollectedDataAndClear()
: 서비스단으로 Receive된 데이터를 리턴한다. 이후 새로운 데이터를 다시 쌓을 수 있도록 초기화를 한다.
2) SMS 서비스 (sms_svc.js
) 📤
이 서비스는 다음과 같은 순서로 SMS를 처리한다:
- Serial 포트 초기화
- SMS 메시지 전송 (
sendSms
) - Serial 포트 Close
import iconv from 'iconv-lite';
import { logger } from '#util/logger.js';
import * as serial from '#module/serial_module.js';
import * as schema from '#schema/sms_schema.js';
import { sleep } from '#util/wb_lib.js';
export const smsService = async (smsData, onErrorCallback) => {
let isSuccess = false;
try {
const smsInfo = {
title: smsData.title,
ment: smsData.ment,
phoneNum: smsData.phoneNum,
};
// smsInfo validation
await schema.SmsInput.validateAsync(smsInfo);
// 시리얼 포트 초기화
await serial.closePort();
await sleep(500);
await serial.openPort();
await sleep(500);
// sms 전송
await sendSms(smsInfo, onErrorCallback);
await sleep(500);
// port close
await serial.closePort();
isSuccess = true;
} catch (error) {
logger.error(`[smsService] Error`);
if (onErrorCallback) {
onErrorCallback(error);
}
throw error;
} finally {
if (!isSuccess) {
await serial.closePort();
}
}
};
코드 설명:
- 라우터에서 Serial 데이터를 받고, 서비스를 처리한다.
- 시리얼 포트는 항상 초기화를 진행해야 하며 에러가 발생해도 무조건 포트는 close를 할 수 있도록 설계를 해야한다.
const sendSms = async (smsInfo, onErrorCallback) => {
let isSuccess = false;
try {
// AT 커멘드 전송
const command = `AT+CMGS="${smsInfo.phoneNum}"\r\n`;
await serial.writePort(Buffer.from(command, 'ascii'));
// AT 커맨드 전송 후 RX Phone Number
let response = await serial.getCollectedDataAndClear();
logger.info(`[sendSms] user phone num: ${response}`);
// sms ment 전송
const smsMent = `${smsInfo.ment}\x1A`;
const encodedSmsMent = iconv.encode(smsMent, 'euc-kr'); // SMS ment를 EUC-KR로 인코딩
await serial.writePort(encodedSmsMent);
logger.info(`[sendSms] sms ment is: ${smsInfo.ment}`);
// sms ment 전송 후 RX sms ment
response = await serial.getCollectedDataAndClear();
if (response.includes('OK')) {
logger.info(`[sendSms] sms ment send Success!`);
}
isSuccess = true;
} catch (error) {
logger.error(`[sendSms] Error`);
if (onErrorCallback) {
onErrorCallback(error);
}
throw error;
} finally {
if (!isSuccess) {
await serial.closePort();
}
}
};
코드 설명:
await serial.writePort(Buffer.from(command, 'ascii'));
: 시리얼 장비마다 다르겠지만, 이 장비는 데이터를 ascii로 전송해야 되어서 string을 ascii로 변환하였다.encodedSmsMent = iconv.encode(smsMent, 'euc-kr');
: 시리얼 장비에서 Receive 받은 데이터는 euc-kr로 인코딩 되어있다. 모듈에서는 parser에서 별다른 인코딩 설정을 하지 않아서 디폴트값인 UTF-8로 되어있다. 그래서 한글 데이터가 들어오면 글자가 깨지는 경우가 발생하여 각 장비에 맞게 인코딩을 일일히 해줘야 한다.response = await serial.getCollectedDataAndClear();
: 서비스단에서 시리얼 모듈의getCollectedDataAndClear()
함수를 호출하여 전송받은 데이터를 확인하고 이에 맞는 비지니스 로직을 수행한다.
5. 정리 🏁
Node.js에서 Serial 통신은 serialport
라이브러리를 통해 구현할 수 있다. 이번 포스팅에서는 실무 코드 예제를 바탕으로 Serial 포트 초기화, 데이터 송수신, 리스너 관리 등의 핵심 기능을 다뤘다.
댓글