728x90

1. 도입: 왜 지금 이 이야기가 화제인가

Hacker News에 올라온 Show HN: I replaced a $120k bowling center system with $1,600 in ESP32s가 1,200점 넘게 받으면서 화제가 됐다. 요약하면 이렇다. 어떤 SRE가 미국 시골의 폐업한 8레인 볼링장을 $105k에 사들였는데, 정작 점수 계산·핀 감지·기계 제어를 담당하는 스코어링 시스템 교체 견적이 $80k~$120k가 나왔다는 것이다. 건물값이랑 맞먹는다.

여기서 실무자 입장에서 눈이 번쩍 뜨이는 대목이 있다. 글쓴이가 뜯어보니 이 6자리 견적짜리 시스템이 70년 된 핀세터 기계에 대해 실제로 하는 일은 릴레이 하나를 딸깍 켜는 것이었다. 나머지는 전부 기계식이다. 카메라 기반 핀 감지, 볼 스피드 계산, 궤적 트래킹 같은 화려한 기능이 붙어 있지만, 기계 제어의 본질은 "단일 릴레이 트리거"였다는 얘기다.

이건 볼링장만의 이야기가 아니다. 우리가 현장에서 만나는 레거시 SCADA, PLC, 오래된 산업용 게이트웨이, 벤더 락인된 관제 시스템 대부분이 이 구조다. 겉은 블랙박스인데 실제 인터페이스는 릴레이 몇 개, 시리얼 몇 줄, 접점 신호 몇 개인 경우가 많다. "이거 우리도 라즈베리파이랑 ESP32로 만들 수 있는 거 아니야?"라는 생각을 실제로 실행에 옮긴 사례라서 참고할 게 많다.

2. 핵심: 그래서 어떻게 동작하는가

레거시 시스템의 실제 구조

원문에서 파악되는 기존 시스템(2008년 설치)의 구성은 이렇다.

  • 카메라 기반 핀 감지 (object detection + 삼각법을 IC 레벨에서)
  • 볼 스피드/궤적 계산
  • 파울 라인 감지(fouling)
  • 애니메이션, 스코어 표시
  • 핀세터 머신 + 볼 리턴 제어 → 결국 릴레이 액추에이션

부품값이 레인 2개당 $4,000. 업그레이드나 서비스 계약은 별도, 모든 기능이 라인 아이템으로 따로 청구되는 전형적인 벤더 락인 구조다. 2008년 기술에 2024년 가격을 받는 셈이다.

ESP32 대체 아키텍처

글쓴이가 만든 OpenLaneLink의 구조를 원문 기준으로 정리하면 이렇다.

[센서/제어 노드들]           [게이트웨이]        [레인 컴퓨터]
ESP32 + 릴레이/옵토커플러  ──ESPNow──▶  ESP32   ──UART──▶  Raspberry Pi
ESP32 + IR break-beam 센서   (스타 토폴로지 메시)          (Redis + 상태머신)
       │                                                        │
       └── RS485 (RF 노이즈 환경 대비 유선 폴백)            React WebSocket
                                                              pub-sub UI

핵심 설계 포인트를 뜯어보자.

  • 노드 계층: 각 ESP32가 센서 이벤트를 방출하고 제어 명령을 수신한다. 노드마다 살짝 다른 펌웨어가 돌아간다(IR 센서 노드, 릴레이 제어 노드 등).
  • 통신 계층: ESPNow 스타 토폴로지 메시가 기본. RF가 지저분한 환경(전기 설비가 계속 서지 튀는 오래된 건물)을 대비해 RS485 유선 폴백을 깔아뒀다. 이 이중화 결정이 실무적으로 핵심이다.
  • 수집 계층: 게이트웨이 ESP32가 UART로 라즈베리파이에 연결. RX 패킷을 파싱해서 Redis에 던진다.
  • 애플리케이션 계층: Redis에 데이터가 들어오면 그 다음부터는 우리에게 익숙한 세계다. WebSocket pub-sub, React UI. "어떤 React 개발자든 자기만의 볼링 애니메이션을 만들 수 있다"는 게 글쓴이 표현이다.

비유하자면, 벤더의 통짜 모놀리식을 엣지(센서/제어) → 게이트웨이(프로토콜 변환) → 이벤트 스트림(Redis) → 프론트로 계층 분리한 것이다. 백엔드 하는 사람이면 마이크로서비스에서 하던 "관심사 분리"를 물리 세계에 그대로 적용했다고 보면 편하다.

IR break-beam으로 핀 감지를 대체한다는 발상

기존 시스템은 카메라 + 객체 탐지로 핀을 셌다. 글쓴이는 IR break-beam 센서로 대체했다. 핀이 서 있으면 빛줄기가 막히고, 넘어지면 통과하는 단순한 원리다. 카메라 비전 파이프라인 대신 GPIO 인터럽트 하나로 끝난다. 아래는 개념을 보여주는 ESP32(Arduino 프레임워크) 펌웨어 스니펫이다.

// pin_sensor_node.ino - IR break-beam 기반 핀 상태 감지 (개념 예시)
#include <esp_now.h>
#include <WiFi.h>

#define NUM_PINS 10
const int sensorPins[NUM_PINS] = {32, 33, 25, 26, 27, 14, 12, 13, 15, 4};
uint16_t pinState = 0;   // bit=1 이면 핀 서있음

// 게이트웨이 MAC (예시)
uint8_t gatewayMac[] = {0x24, 0x6F, 0x28, 0xAA, 0xBB, 0xCC};

typedef struct {
  uint8_t  msgType;   // 0x01 = pin_state
  uint16_t pins;      // 10비트 사용
  uint32_t seq;
} __attribute__((packed)) PinMsg;

uint32_t seqCounter = 0;

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < NUM_PINS; i++) pinMode(sensorPins[i], INPUT_PULLUP);

  WiFi.mode(WIFI_STA);
  if (esp_now_init() != ESP_OK) {
    Serial.println("ESPNow init failed");
    return;
  }
  esp_now_peer_info_t peer = {};
  memcpy(peer.peer_addr, gatewayMac, 6);
  peer.channel = 1;
  peer.encrypt = false;
  esp_now_add_peer(&peer);
}

void loop() {
  uint16_t current = 0;
  for (int i = 0; i < NUM_PINS; i++) {
    // break-beam: LOW = 빔 차단 = 핀 서있음
    if (digitalRead(sensorPins[i]) == LOW) current |= (1 << i);
  }
  // 상태가 바뀔 때만 전송 (엣지 트리거)
  if (current != pinState) {
    pinState = current;
    PinMsg msg = { 0x01, pinState, seqCounter++ };
    esp_now_send(gatewayMac, (uint8_t*)&msg, sizeof(msg));
    Serial.printf("pin_state=0x%03X seq=%u\n", pinState, seqCounter);
  }
  delay(20);  // 디바운스 겸
}

이걸 플래시하고 시리얼 모니터를 열면 대략 이런 출력이 나온다.

$ pio device monitor -b 115200
--- Terminal on /dev/ttyUSB0 | 115200 8-N-1
pin_state=0x3FF seq=1
pin_state=0x3FE seq=2
pin_state=0x37A seq=3
pin_state=0x000 seq=4

0x3FF은 10핀 모두 서있음(스페어 프레임 시작), 0x000은 스트라이크로 다 넘어간 상태다. 이렇게 상태 변화만 이벤트로 흘려보내면 대역폭도 아끼고 라즈베리파이 쪽 상태머신도 깔끔해진다.

Redis로 이벤트를 받아 상태를 관리하기

게이트웨이 ESP32가 UART로 넘긴 이벤트를 라즈베리파이가 파싱해서 Redis Pub/Sub으로 흘린다. 파이썬 쪽 골격은 이 정도다.

import redis, serial, struct

r = serial.Serial('/dev/ttyAMA0', 115200, timeout=1)
rds = redis.Redis(host='localhost', port=6379, decode_responses=True)

def parse(frame: bytes):
    # msgType(1) + pins(2, LE) + seq(4, LE)
    msg_type, pins, seq = struct.unpack('<BHI', frame[:7])
    return msg_type, pins, seq

while True:
    line = r.read(7)
    if len(line) < 7:
        continue
    mt, pins, seq = parse(line)
    if mt == 0x01:
        # 채널로 publish + 최신 상태 저장
        rds.publish('lane:3:pins', pins)
        rds.set('lane:3:pins:latest', pins)
        print(f"[lane3] pins=0x{pins:03X} seq={seq}")

구독 쪽은 redis-cli로 바로 확인 가능하다.

$ redis-cli subscribe lane:3:pins
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "lane:3:pins"
3) (integer) 1
1) "message"
2) "lane:3:pins"
3) "1018"     # 0x3FA
1) "message"
2) "lane:3:pins"
3) "0"        # 스트라이크

여기까지 오면 프론트엔드는 그냥 WebSocket으로 이 채널을 구독해서 애니메이션 트리거하면 된다. 물리 세계 이벤트가 완전히 소프트웨어 문제로 환원된 것이다. 이 지점이 이 아키텍처의 진짜 강점이다.

3. 실무 관점: 도입 시 고려사항과 흔한 함정

트레이드오프 1: RF vs 유선, 왜 RS485 폴백을 깔았나

글쓴이가 "전기 설비가 계속 서지를 튄다"고 했다. 오래된 산업 환경에서 ESPNow(2.4GHz) 하나만 믿으면 안 되는 이유다. 핀세터 모터가 돌 때 나오는 EMI, 형광등 안정기, 낡은 배선에서 나오는 노이즈가 2.4GHz 대역을 그냥 갉아먹는다. 그래서 무선 우선 + 유선 폴백 이중화가 정석이다.

여기서 실무 판단이 갈린다. 레인이 8개고 노드가 수십 개인데 전부 RS485로 깔면 배선 지옥이 된다. 반대로 무선만 쓰면 결정적 순간(스트라이크 감지)에 패킷이 유실된다. 글쓴이의 선택—평소엔 ESPNow, 노이즈 심하면 RS485—은 비용과 안정성의 합리적 절충으로 보인다.

흔한 함정 1: ESPNow 채널 불일치와 peer add 실패

ESPNow에서 가장 많이 밟는 지뢰가 Wi-Fi 채널 불일치다. 송신·수신 노드의 채널이 다르면 조용히 패킷이 안 간다. 로그도 안 남아서 삽질하기 딱 좋다. 또 하나는 peer를 안 넣고 send를 때리는 경우다.

E (12453) ESPNOW: Peer not found, add peer first
E (12453) ESPNOW: esp_now_send failed, ret=12395

ret=12395ESP_ERR_ESPNOW_NOT_FOUND 계열이다. 대상 MAC을 esp_now_add_peer()로 먼저 등록했는지, 그리고 STA 모드에서 채널을 명시적으로 고정했는지 확인해야 한다. AP에 붙는 순간 채널이 바뀌어버리는 것도 함정이다. 그래서 순수 ESPNow 메시라면 esp_wifi_set_channel()로 채널을 박아버리는 게 안전하다.

흔한 함정 2: UART 프레이밍 깨짐

게이트웨이 ESP32 → 라즈베리파이 UART 구간에서 위 파이썬 예제처럼 read(7)로 고정 길이만 믿으면 반드시 언젠가 프레임이 밀린다. 노이즈로 1바이트가 씹히면 그 뒤로 모든 프레임이 어긋난다. 시리얼 모니터에 이런 게 찍히면 프레이밍 문제다.

[lane3] pins=0x2AF seq=1
[lane3] pins=0xFF3A seq=3355443    # seq가 갑자기 튐 = 프레임 밀림
struct.error: unpack requires a buffer of 7 bytes

대응은 정석대로다. 스타트 바이트(예: 0xAA) + 길이 + 페이로드 + CRC 프레이밍을 넣고, 수신 측에서 스타트 바이트를 찾아 재동기화하는 로직을 둔다. 이거 안 하고 데모까지는 잘 돌다가 현장 나가면 터진다. 물리 계층 붙이는 프로젝트의 90% 함정이 여기다.

트레이드오프 2: 안전(safety) 문제는 별개다

이건 원문에 명시된 건 아니지만 실무자로서 반드시 짚어야 한다. 볼링 핀세터는 회전하는 무거운 기계다. 릴레이를 SW로 제어한다는 건 곧 버그가 물리적 사고로 직결될 수 있다는 뜻이다. 댓글에서도 예전에 "핀세터가 두 번 연속 리셋되는 글리치가 있었다, 릴레이가 너무 오래 닫혀 있었던 것 같다"는 경험담이 나온다.

ESP32가 죽거나(WDT 리셋), 프리징되거나, 릴레이 GPIO가 부팅 순간 HIGH로 튀는(ESP32의 특정 스트래핑 핀은 부팅 시 상태가 불안정하다) 상황을 반드시 설계에 반영해야 한다. 실제 상용 전환이라면:

  • 기계 자체의 기계식 리미트 스위치/E-Stop은 그대로 살려둔다(SW가 우회 못 하게).
  • 릴레이는 워치독 하드웨어로 페일세이프(전원 나가면 OFF) 걸리게.
  • 부팅 시 GPIO가 뜨는 핀은 릴레이 제어에 쓰지 않는다.

취미로 내 볼링장 돌리는 것과, 이걸 오픈소스로 뿌려서 남들이 자기 시설에 붙이는 건 안전 책임 레벨이 완전히 다르다. OpenLaneLink가 상용 수준으로 가려면 이 부분이 가장 어려운 관문일 것으로 보인다.

대안은 없나

  • ScoreMore 같은 기존 SW와 통합: 원문 댓글의 vikbez가 실제로 한 방식이다. 하드웨어는 아두이노로 바꾸되 검증된 스코어링 SW에 붙였다. 글쓴이도 검토했으나 파울 감지 유닛 하나가 $750인 게 걸려서 완전 자작으로 갔다고 한다. 리스크를 줄이려면 통합이 합리적이다.
  • 산업용 PLC + HMI: 안정성·인증 측면에선 정석이지만 비용이 다시 올라간다. 벤더 락인의 다른 버전이 될 수 있다.
  • ESP32 대신 라즈베리파이 GPIO 직결: 노드가 적으면 오히려 단순하다. 다만 레인마다 배선 끌고 가야 해서 분산 환경엔 안 맞는다.

비용 구조: CapEx만 보면 안 된다

원문 수치를 정리하면 이렇다(글쓴이 주장 기준).

  • 벤더 1:1 교체: $80k ~ $120k (업그레이드/서비스 계약 별도)
  • 벤더 부품: 레인 2개당 $4,000
  • 자작 프로토타입: 레인 2개당 약 $200 (풀옵션 $400)
  • 수리 시간: 5분 / 레인 페어 교체: 10분 이내

여기서 인프라 엔지니어가 놓치기 쉬운 포인트. CapEx($200 vs $120k)만 보면 압도적이지만, 진짜 가치는 운영 비용과 소유권에 있다. 부품을 서랍에 미리 플래시해서 쟁여둘 수 있고, 벤더 콜 없이 5분이면 고친다. 데이터를 내가 소유한다. Tron 테마로 바꾸고 싶으면 그냥 React 컴포넌트 갈아끼우면 된다. 벤더 시스템에선 이게 다 "새 라인 아이템"이었다.

반대로 숨은 비용도 정직하게 봐야 한다. 펌웨어·프로토콜 작성이 "진짜 어려운 부분"이라고 글쓴이 본인이 말한다. 이 엔지니어링 시간(글쓴이는 SRE다)이 공짜가 아니다. 사내에 이 역량이 없으면 $120k 견적이 오히려 싸게 느껴질 수도 있다. 레거시 현대화의 손익분기는 언제나 "우리 팀이 이걸 유지보수할 수 있는가"에서 갈린다.

4. 정리: 한 줄 요약과 적용 대상

한 줄 요약: 겉이 화려한 레거시 산업 시스템도 실제 인터페이스는 릴레이·시리얼·접점 몇 개인 경우가 많고, ESP32 + 이벤트 스트리밍(Redis) 조합으로 계층을 분리하면 비용을 두 자릿수 배수로 줄이면서 소유권과 유연성을 되찾을 수 있다.

이럴 때 써라

  • 레거시 시스템의 실제 I/O가 단순한데(릴레이/접점/시리얼) 벤더 견적만 비싼 경우
  • 사내에 임베디드/펌웨어를 감당할 인력이 있고, 장기 유지보수 의지가 있는 경우
  • 안전 리스크가 낮거나, 기계식 안전장치가 별도로 살아있는 경우

이럴 땐 다시 생각하라

  • 인명·안전 인증이 필수인 시스템(SW 버그 = 물리 사고). 페일세이프 설계와 검증에 드는 비용이 CapEx 절감분을 넘길 수 있다.
  • 사내에 펌웨어·프로토콜 유지보수 역량이 없어서 결국 또 다른 "1인 의존" 락인이 되는 경우
  • 다운타임 허용치가 극도로 낮은 미션 크리티컬 라인

개인적으로 가장 인상 깊었던 건 "물리 이벤트를 Redis 채널로 밀어넣는 순간, 그 뒤는 우리가 매일 하는 웹 개발 문제가 된다"는 접근이다. 백엔드/인프라 엔지니어가 임베디드를 두려워하지 말아야 하는 이유가 여기 있다. 어려운 건 물리

728x90

+ Recent posts