문제 상황
PC에서 시리얼 통신을 하기 위해서는 연결된 포트(comport)를 지정해야 한다. 즉, 시리얼 통신을 위해서는 우선 comport를 알아야 하고 그렇기 위해서는 PC에 현재 연결되어 있는 포트를 알 필요가 있다. 물론 사용자로부터 직접 입력을 받는 방법도 있지만 좀 더 편리한 유저 인터페이스를 위해 PC에 연결된 시리얼 포트 목록을 윈도우에서 사용하는 식별자(COM1, COM2...)로 표현하도록 구현하고자 했다. 결과 코드를 보면 다음과 같다.
func GetPortNames() ([]string, error) {
key, err := registry.OpenKey(windows.HKEY_LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM\`, windows.KEY_READ)
if err != nil {
if errors.Is(err, syscall.ERROR_FILE_NOT_FOUND) {
return []string{}, nil
}
return nil, fmt.Errorf("failed to get comport - %s", err)
}
defer key.Close()
list, err := key.ReadValueNames(0)
if err != nil {
return nil, fmt.Errorf("failed to parse comport list - %s", err)
}
result := make([]string, 0)
for _, device := range list {
value, _, err := key.GetStringValue(device)
if err == nil {
result = append(result, value)
}
}
return result, nil
}
해당 코드가 나오기까지 많은 시행착오가 있었는데 이번 글에서는 문제 해결 과정을 설명하고자 한다.
해결 과정
외부 라이브러리
가장 먼저 살펴본 것은 외부 라이브러리였다. 물론 외부 라이브러리에 의존하는 것이 좋은 것은 아니지만 이 기능은 프로그램의 핵심 기능도 아닐 뿐더러 다른 시리얼 통신 관련 작업 역시 외부 라이브러리를 사용하고 있었으므로 우선적으로 고려했다. 예상하셨다시피 라이브러리를 이용하는 방법은 실패했지만 - 성공했다면 이 글이 나오지도 않았을 것이다 - 나중에 문제 해결의 실마리가 되었다.
우선 현재 사용 중인 라이브러리는 github.com/tarm/serial이다. 통신에 쓸 데에는 부족함이 없었지만 아쉽게도 현재 연결된 포트 목록을 보여주는 기능이 없었다. 다음으로 가장 많이 쓰이는 라이브러리인 go.bug.st/serial을 찾아보았다. 일단 해당 라이브러리에 원하는 기능과 비슷해보이는 GetPortList 함수가 있었다. 해당 라이브러리에서 본 코드는 다음과 같다.
func nativeGetPortsList() ([]string, error) {
key, err := registry.OpenKey(windows.HKEY_LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM\`, windows.KEY_READ)
switch {
case errors.Is(err, syscall.ERROR_FILE_NOT_FOUND):
// On machines with no serial ports the registry key does not exist.
// Return this as no serial ports instead of an error.
return nil, nil
case err != nil:
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
defer key.Close()
list, err := key.ReadValueNames(0)
if err != nil {
return nil, &PortError{code: ErrorEnumeratingPorts, causedBy: err}
}
return list, nil
}
그러나 해당 함수는 COM1, COM2와 같이 사용자가 사용하는 명칭이 아닌 해당 포트의 절대 경로를 출력하는 것으로 확인되었다. 이는 필자가 원하던 기능이 아니었기에 해당 라이브러리 역시 사용하지 않았다. 다만 위의 코드는 문제 해결의 열쇠가 되었는데, 당장 완성 코드와도 비슷한 부분이 눈에 보일 것이다. 이는 후술하도록 하겠다.
외부 프로그램
다음으로 찾은 것은 다른 언어에서의 구현체였다. 혹시 명령어나 외부 언어의 구현체만 불러다가 쓰면 되지 않을까란 생각이었다.
C#의 경우는 역시 윈도우와 연결되어 있는 언어이다 보니 해당 기능을 위한 내장 라이브러리가 있었다.
using System;
using System.IO.Ports;
namespace SerialPortExample
{
class SerialPortExample
{
public static void Main()
{
// Get a list of serial port names.
string[] ports = SerialPort.GetPortNames();
Console.WriteLine("The following serial ports were found:");
// Display each port name to the console.
foreach(string port in ports)
{
Console.WriteLine(port);
}
Console.ReadLine();
}
}
}
파이썬 역시 포트 목록을 가져오는 방법이 있었다.
import serial.tools.list_ports
ports = serial.tools.list_ports.comports()
print([port.name for port in ports])
채택하지는 않았지만 정말 방법이 없었다면 위의 코드를 빌드해서 넣거나(C#) 스크립트를 통째로 python 명령어에 넣는 식으로(Python)으로 구현했을 것이다. 이렇게 될 경우 나중에 프로그램을 관리하기 어려워 질 것이 자명해 보였기 때문에 최후의 보루로 남겨뒀다.
레지스트리
다시 찾고 있는 기능에 비교적 근접했던 go.bug.st/serial 라이브러리의 구현 부분을 보았다. 이 중 'HARDWARE\DEVICEMAP\SERIALCOMM\'이라는 부분이 눈에 띄어서 검색해보니 윈도우 레지스트리 관련 내용이 뜨면서 시리얼 포트와 관련 있어 보이는 글을 발견했다. 해당 글의 말에 따르면 현재 사용할 수 있는 시리얼 포트는 레지스트리에 저장되어 있으며 해당 레지스트리를 참조하면 현재 연결되어 있는 포트 목록을 알 수 있다. 그리고 확인 결과 현재 이용 가능한 시리얼 포트 목록과 레지스트리에 저장되어 있는 값이 맞음을 확인할 수 있었다. 레지스트리 편집기에서 해당 주소로 이동할 경우 다음과 같이 보인다
다시 라이브러리의 코드를 보면 해당하는 키의 값들을 읽어서 반환하는 것을 볼 수 있다. 하지만 원하는 것은 경로가 아니라 우측에 있는 포트 이름이었다. 그래서 해당하는 키에 대한 data 값을 받는 로직을 추가했고 성공적으로 동작하였다.
마치며
우선 원했던 기능을 찾을 수 없었던 이유는 자체를 시리얼 포트 이름(COM1, COM2...)을 쓰는 곳이 윈도우 밖에 없었기 때문이였다. 다른 운영체제에서는 라이브러리의 것처럼 경로를 식별자로 이용한다. 즉, 라이브러리는 대부분의 운영체제에서 범용적으로 쓰이는 것을 목적으로 하기 때문에 해당하는 함수가 없었던 것이다.
해결 과정에서 아쉬웠던 점은 중간에 라이브러리 코드가 어떻게 동작하는지 조금 더 유심히 보고 판단했으면 빨리 끝냈을 것 같다는 점이었다. 실제로 외부 언어에서 레퍼런스를 찾으면서 빌드해서 쓸지 실험해본 것에 거의 반나절이 걸렸다. 물론 이러한 시행착오가 있었기 때문에 확실하게 공부가 되었다고는 생각한다.
여담이지만 해당 구현체에는 한가지 사소한 문제점이 있다. 실제 장비를 연결할 때에는 정상적으로 동작했지만 가상 시리얼 포트 프로그램을 통해 만든 가상 포트는 인식하지 못하는 문제가 있다. 아마 레지스트리를 통해 판단하는 로직 특성상 가상 시리얼 포트 프로그램에서는 따로 드라이버를 설치하지 않으므로 레지스트리에 값을 쓰지 않아서 발생하는 문제 같았다. 필자의 상황에서는 이로 인해 발생하는 문제는 없었지만 혹시 위의 코드를 쓴다면 이 점만 주의해주면 좋을 것 같다.