준PM
☕ 커피챗 오픈
🏅 베스트오브베스트

선풍기에 영혼 불어넣기 — 조립보다 길었던 AI·아두이노 연결기

> 한 줄 요약: 택배로 도착한 OLED를 아두이노에 연결하고 Gemini 기능 탑재, 와 음악 감지기능까지 살려냈다. 아직 MOSFET이 없어 진짜 바람 조절까지는 안되지만, 선풍기의 ‘뇌와 표정’은 먼저 완성됐다.

## 지난 이야기: 부품만 도착해라, 바로 완성해주리라

지난 번에는 방구석에서 자리만 차지하던 애물단지 USB 선풍기에 Gemini AI를 얹을 아이디어를 구체화했다. 음악이 들리면 신나게 반응하고, 사용자가 “버그 때문에 답답해 죽겠어!”라고 말하면 스트레스 지수를 분석해 강풍으로 머리를 식혀주는 분노조절선풍기.

아이디어를 확정하고, 책상 위에서 돌아다니던 Arduino UNO를 잡아오고, 장바구니에 잘못 담은 3만 원짜리 해외직구 부품을 2,500원짜리 국내부품으로 바꾸는 데까지가 지난 이야기였다. 마지막에는 자신 있게 이렇게 적었다.

> “택배가 도착하면 브레드보드에 선들을 쏙쏙 꽂아 회로를 완성한다.”

그때는 몰랐다. 선 네 개를 꽂는 일보다 Windows의 COM 포트와 드라이버를 설득하는 일이 훨씬 오래 걸릴 줄은.

## 이번 사례는 이런 경우에 도움이 됩니다

- Arduino 호환 보드를 처음 PC와 연결해본다

- CH340 보드가 처음에는 연결되다가 다시 열면 끊어진다

- Gemini API와 Arduino를 Python으로 연결해보고 싶다

- 코드는 분명히 실행되는데 하드웨어에서는 아무 반응이 없어 답답하다

- 부품이 모두 준비되지 않아도 프로젝트를 단계적으로 검증해보고 싶다

## 1️⃣ MOSFET은 없지만, 할 수 있는 것부터 시작했다

주문한 부품 중 0.96인치 OLED 디스플레이와 점퍼선은 도착했지만, 선풍기 모터의 전원을 제어할 IRF520 MOSFET 모듈은 일시 품절이라는 메세지가 왔다. MOSFET이 없으면 Arduino의 작은 출력신호로 USB 선풍기의 실제 전원을 안전하게 켜고 끄거나 속도를 제어할 수가 없다.

MOSFET이 올 때까지 작업을 멈추고 기다려도 될 법 했지만, 언제올지 모를 녀석을 마냥 기다릴 수도 없는 노릇이라 이걸 제외한 나머지 파이프라인을 완성해보기로 했다. 실제 팬 속도 대신 Arduino 13번 내장 LED를 켜고 끄도록 펌웨어를 만들면, 적어도 PC → 시리얼 통신 → Arduino → OLED/LED 흐름은 검증할 수 있기 때문이다.

결과적으로 이 선택이 옳았다. 하드웨어 한 부품의 배송이 늦어도 소프트웨어와 통신 문제를 먼저 찾아낼 수 있었고, 나중에 MOSFET이 도착하면 마지막 출력부만 연결하면 되는 구조가 됐다.

## 2️⃣ OLED 선 네 가닥, 드디어 AI가 표정을 지었다

도착한 OLED 모듈을 살펴보니 구멍이 있는 암놈 헤더가 아니라 핀이 튀어나온 수놈 헤더 였다. 빵판을 거칠 필요없이 암-수 점퍼선 네 개로 Arduino에 바로 연결했다.

| OLED | Arduino UNO |

| ---- | ----------- |

| GND | GND |

| VCC | 5V |

| SCL | A5 |

| SDA | A4 |

Arduino IDE 2.3에서 Adafruit SSD1306, Adafruit GFX 라이브러리를 설치하고 펌웨어를 업로드했다. 펌웨어의 역할은 단순하다. PC에서 아래처럼 한 줄짜리 명령을 받는다.

```text

200,MUSIC

255,ANGRY

0,IDLE

```

앞의 숫자는 팬 속도에 해당하는 0~255 값이고, 뒤의 단어는 OLED에 띄울 표정 태그다. 아직 실제 모터는 없으므로 속도가 100을 넘으면 13번 내장 LED가 켜지도록 했다.

업로드 직후 OLED에 AI FAN / Waiting PC...가 나타났다. 처음으로 텍스트 화면 안에만 있던 AI가 물리적인 얼굴을 얻은 순간이었다. 아직 바람은 없었지만, 적어도 선풍기의 눈은 떠지게 한 거다.

## 3️⃣ Python 파일은 왜 실행조차 안 됐을까

이제 PC의 Python 스크립트를 실행해 Gemini와 Arduino를 연결하면 될 줄 것이었다. 하지만 첫 실행부터 오류가 이어졌다.

### NumPy DLL부터 삐걱거렸다

음악 RMS 계산에 사용하는 NumPy가 Anaconda의 Intel MKL 라이브러리와 충돌하면서 _multiarray_umath DLL 로드 오류가 발생했다. NumPy와 MKL 관련 패키지를 다시 설치해 해결했다.

```powershell

pip uninstall -y numpy

pip install --force-reinstall numpy mkl mkl-service

```

패키지 문제를 고치고 나니 이번에는 Arduino 포트 연결이 막혔다.

### COM3은 Arduino가 아니라 Bluetooth였다

Gemini는 Arduino 포트가 COM3이라고 했고 Python 코드도 PORT = 'COM3'으로 되어 있었다. 그런데 아무리 해도 동작을 하지 않아서 Windows의 포트를 하나씩 확인해보니 결과가 달랐다.

- COM3, COM4: Bluetooth 가상 직렬 포트

- COM7: USB-SERIAL CH340, 실제 Arduino 호환 보드

코드는 엉뚱한 Bluetooth 포트에 계속 말을 걸고 있었던 셈이다. 더 재미있는 것은 COM3 연결실패 자체가 마지막 오류가 아니었다는 점이다. 오류를 알려주려고 출력한 이모지를 Windows CP949 콘솔이 표현하지 못하면서 UnicodeEncodeError가 한 번 더 발생했다.

그래서 포트를 특정 번호로 고정하지 않고 CH340 또는 USB 시리얼 장치를 자동 탐지하도록 바꿨다. 콘솔 메시지의 이모지도 [OK], [오류], [안내] 같은 안전한 텍스트로 교체했다.

## 4️⃣ 한 번은 열리는데 두 번째는 안 열린다: CH340의 함정

포트를 바로잡자 COM7이 한 번은 정상적으로 열렸다. 그런데 프로그램을 종료하고 다시 실행하면 이런 오류가 나타났다.

```text

Cannot configure port

can't set com-state

```

USB를 뺐다가 다시 꽂으면 COM8이라는 새 번호로 나타나 첫 연결은 또 성공했다. 하지만 닫았다가 다시 열면 같은 문제가 반복됐다. 포트 점유 문제처럼 보였지만 Arduino IDE의 시리얼 모니터와 플로터는 이미 닫혀 있었다.

Windows 장치 상태와 드라이버를 확인한 결과 원인은 CH340 드라이버 3.9.2024.9와 특정 CH340 칩의 호환 문제였다. Arduino 공식 도움말에서도 최신 드라이버에서 같은 증상이 발생할 수 있으며 이전 버전인 3.7.2022.1 설치를 안내하고 있었다.

공식 안내에 연결된 이전 드라이버를 받아 전자서명을 검증한 뒤 설치했다. 그다음 같은 COM8 포트를 연속으로 두 번 열어 0,IDLE 명령을 각각 보냈다.

```text

open 1 True

sent 1

open 2 True

sent 2

```

두 번 모두 성공했다. 처음에는 Python 코드 문제처럼 보였지만, 실제 원인은 USB 시리얼 드라이버였다. 하드웨어 프로젝트에서는 “코드가 맞는가?”만큼 “운영체제가 그 장치를 어떤 드라이버로 다루는가?”도 중요하다는 것을 제대로 배웠다.

## 5️⃣ Gemini는 대답했지만, 새 API 키로 대답한 건 아니었다

Gemini API 키는 소스 코드에 직접 적지 않고 Windows 사용자 환경변수 GEMINI_API_KEY에 저장했다. 키가 Git이나 공유 문서에 노출되는 일을 피하기 위해서였다.

그런데 새 키로 테스트했는데 SDK가 이런 경고를 냈다.

```text

Both GOOGLE_API_KEY and GEMINI_API_KEY are set.

Using GOOGLE_API_KEY.

```

PC에 예전에 설정한 GOOGLE_API_KEY가 남아 있었고, google-genai SDK가 새 키보다 기존 키를 우선 사용한다는는 것이다. 응답이 성공했다고 해서 의도한 설정이 검증된 것은 아니었다.

프로그램 실행 중에는 기존 GOOGLE_API_KEY를 제외하고 사용자가 지정한 GEMINI_API_KEY를 확실히 사용하도록 수정했다. 또한 새로 연 터미널이 아니어도 Windows 사용자 환경변수 저장소에서 키를 직접 읽을 수 있게 보완했다.

최종 테스트 문장은 다음과 같았다.

```text

I am frustrated because of a bug.

```

Gemini 2.5 Flash의 판단 결과는 스트레스 80%, 팬 속도 200, 표정 ANGRY였다. Python은 이를 200,ANGRY로 바꾸어 COM8로 전송했고 OLED와 내장 LED가 반응했다. 드디어 사용자의 감정 → Gemini의 판단 → 물리 장치의 표정이라는 첫 번째 모드가 완성됐다.

## 6️⃣ 음악이 들려오쟎아, 넌 왜 반응하지 않니

두 번째 모드도 처음에는 조용했다. 음악을 틀어도 오류는 없고, Arduino도 아무 반응이 없었다.

기존 코드는 다음 방식으로 음량을 계산했다.

```python

volume_norm = np.linalg.norm(indata) * 10

if volume_norm > 15:

...

```

이 값은 오디오 블록 크기에 따라 달라지는데, 알고보니 현재 PC 마이크의 실제 신호에 비해 기준 15는 너무 높았다. 게다가 sounddevice.InputStream은 기본적으로 스피커에서 재생되는 디지털 음원 자체가 아니라 마이크로 들어오는 소리를 듣는다. 헤드폰으로 음악을 들으면 아무리 크게 재생해도 마이크에는 음악이 들어오지 않는다.

그래서 계산을 표준 RMS 방식으로 바꾸고, 이 PC에서 측정한 마이크 잡음과 실제 소리 크기를 바탕으로 기본 임계값을 0.0002까지 낮췄다.

```python

rms = float(np.sqrt(np.mean(np.square(indata))))

```

또한 프로그램이 침묵하지 않도록 현재 입력 장치와 실시간 음량을 1초마다 보여주게 했다.

```text

[입력 장치] Microphone Array...

[마이크 음량] 0.0012 / 기준 0.0002 (감지)

[아두이노 전송] 속도: 85 | 표정: MUSIC

```

음악이 멈춘 뒤 0.7초가 지나면 0,IDLE을 보내 OLED와 출력 상태도 자동으로 쉬게 했다. 실제 음악을 틀어 테스트하자 RMS 값이 기준을 넘었고 MUSIC 명령이 Arduino로 전달됐다. 두 번째 모드도 마침내 정상 작동했다.

## 7️⃣ 끝난 줄 알았는데 OLED가 까맣다: 기록과 실물은 다를 수 있다

두 모드가 터미널에서는 정상 작동한 뒤, 완성 상태를 사진으로 살펴보다가 이상한 점을 발견했다. Arduino의 전원 LED는 환하게 켜져 있는데 정작 왼쪽 OLED는 새까맣게 꺼져 있었다.

LCD 화면이 부착된 아두이노 보드

처음에는 사진 각도 때문에 전원선이나 A4/A5 배선이 잘못된 것처럼 보였다. 더 가까이에서 여러 각도로 다시 촬영해 핀 이름을 하나씩 대조하니 최종 배선은 정상이었다.

```text

GND → GND

VCC → 5V

SCL → A5

SDA → A4

```

“MOSFET이 아직 없어서 OLED도 안 켜지는 것 아닐까?”라는 의문도 들었지만, IRF520은 팬 모터의 전원을 제어하는 스위치일 뿐 OLED와는 독립된 부품이다. MOSFET이 없어도 OLED는 정상적으로 켜져야 한다.

남은 가능성은 세 가지였다. OLED 패널 불량, I2C 주소 불일치, 그리고 펌웨어 문제였다. 그런데 프로젝트 폴더를 확인해보니 중요한 단서가 나왔다. Python 파일은 보관되어 있었지만 Arduino의 .ino 원본 파일이 하나도 없었다. 기록에는 펌웨어 업로드 성공이라고 적혀 있었지만, 현재 보드에 어떤 스케치가 남아 있는지는 재현할 수 없는 상태였다.

### I2C 주소부터 직접 물어봤다

Arduino IDE에 포함된 CLI와 설치 라이브러리를 확인했다. 다행히 Arduino AVR 코어와 Adafruit SSD1306, Adafruit GFX, Adafruit BusIO가 모두 설치되어 있었다.

OLED 전용 진단 스케치를 만들어 COM8에 올렸다. 스케치는 A4/A5의 모든 I2C 주소를 검색하고, 발견된 장치를 SSD1306으로 초기화한 뒤 화면에 테스트 문구와 카운터를 표시하도록 했다.

결과는 명확했다.

```text

I2C_FOUND:0x3C

DIAG_RESULT:OLED_INIT_OK

HOST_RESULT:OLED_COMMUNICATION_OK

```

잠시 뒤 검은 화면에 OLED TEST OK, 주소 0x3C, 증가하는 숫자가 나타났고 화면도 1초마다 반전됐다. OLED도, 전원도, 배선도 모두 정상이었다. 제미나이는 하드웨어 문제가 아니라 현재 제어 펌웨어를 확인하고 복원할 원본이 없었던 것에 가까웠다고 알려줬다.

LCD가 부착된 아두이노 보드

### 이번에는 펌웨어도 프로젝트 자산으로 남겼다

진단이 끝난 뒤 Python 명령 규격과 연결되는 제어 펌웨어를 새로 작성했다. smart_fan_controller.ino200,ANGRY 같은 명령을 받아 OLED 표정, 상태 문구, 속도 숫자와 막대를 표시하고 내장 LED를 켠다. 처리한 명령은 ACK:200,ANGRY처럼 다시 PC로 회신한다.

복원 후 다섯 가지 명령을 연속으로 보냈다.

```text

ACK:0,IDLE

ACK:200,ANGRY

ACK:160,MUSIC

ACK:120,COOL_DOWN

ACK:0,IDLE

```

모든 명령이 확인됐고 마지막에는 다시 대기 화면으로 돌아왔다. 이번에는 진단용 스케치와 실제 제어 스케치를 모두 아두이노 프로젝트 폴더에 저장했다. “작동했다”는 기억만 남긴 것이 아니라, 나중에도 다시 컴파일하고 업로드할 수 있는 형태로 남긴 것이다.

이 후일담에서 얻은 교훈은 단순했다. 진행상황 보고서의 성공 기록보다 현재 실물의 검은 화면이 더 정직하며, 실행 파일만큼 원본 펌웨어를 함께 보관하는 것이 중요하다.

## 결과: 아직 바람은 없지만 영혼은 들어왔다

현재까지 완성된 흐름은 다음과 같다.

```text

사용자 텍스트 ─→ Gemini 감정 분석 ─┐

├─→ Python ─→ COM8 ─→ Arduino ─→ OLED + LED

마이크 음악 ───→ 실시간 RMS 분석 ───┘

```

- OLED 배선과 표시 정상

- I2C 주소 0x3C 및 SSD1306 초기화 실물 검증 완료

- 진단용·제어용 Arduino .ino 원본 프로젝트 보관

- Arduino 펌웨어 업로드 및 시리얼 명령 수신 정상

- Gemini 감정 분석 모드 정상

- 음악 비트 감지 모드 정상

- 프로그램 종료 및 음악 정지 시 0,IDLE 초기화 정상

- CH340 드라이버와 COM 포트 재연결 문제 해결

처음에 계획했던 대로로 “생각은 PC가, 행동은 선풍기가”라는 구조가 실제로 작동했다. PC는 Gemini와 오디오 분석을 담당하고, Arduino는 아주 짧은 명령만 받아 표정과 출력을 제어한다. 복잡한 AI를 작은 보드 안에 억지로 넣지 않아도 물리적 AI 에이전트를 만들 수 있다는 것을 확인했다.

다만 아직 진짜 바람은 불지 않는다. IRF520 MOSFET 모듈이 일시 품절이라 USB 선풍기의 모터 전원부는 연결하지 않았기 때문이다. 현재는 OLED와 13번 LED가 팬의 감정과 속도를 대신 표현하고 있다.

Arduino가 포함된 Arduino 팬 컨트롤러 Arduino Arduino Arduino ard

## 이번에 배운 점 📚

### 1. 부품이 하나 없어도 전체 프로젝트를 멈출 필요는 없다

모터 대신 LED를 임시 출력으로 사용하니 통신과 AI 로직을 먼저 완성할 수 있었다. 하드웨어 프로젝트도 기능을 층별로 나누면 배송지연과 부품부족에 덜 흔들린다.

### 2. 오류메시지의 처음 원인이 진짜 원인이라는 보장은 없다

COM3 오류 뒤에 CP949 출력 오류가 숨어 있었고, COM8 재접속 오류 뒤에는 CH340 드라이버 호환 문제가 있었다. 오류를 한 덩어리로 보지 않고 계층별로 분리해 확인하는 것이 중요했다.

### 3. “응답이 왔다”와 “의도한 키로 응답이 왔다”는 다르다

Gemini가 정상 응답해도 SDK가 다른 환경변수를 사용하고 있을 수 있었다. 성공 여부뿐 아니라 어떤 설정과 경로가 실제로 사용됐는지도 확인해야 한다.

### 4. 센서 값은 이론적인 기준보다 내 환경의 실측값이 중요하다

음악 감지 임계값 15는 코드상 그럴듯했지만 현재 마이크에서는 사실상 반응 불가능한 값이었다. 실시간 값을 화면에 보이게 만들고 실제 잡음과 소리를 측정한 뒤 기준을 정하자 바로 해결됐다.

### 5. AI는 코드를 만드는 도구라기 보다는, 함께 진단하는 동료에 가까웠다

이번 작업에서 제미나이는 Python 파일만 고친 것이 아니다. 포트 목록, 장치 종류, 드라이버 버전, API 키 우선순위, 마이크 RMS를 차례로 확인하고 매 단계에서 실제 실행 결과를 다음 판단의 근거로 삼았다. 하드웨어 초보에게 가장 유용했던 것은 정답 코드 한 덩어리보다 “지금 어느 계층에서 막혔는지 함께 좁혀가는 과정”이었다.

## 재사용 가능한 문제 해결 프롬프트

비슷한 Arduino·Python 연동 문제를 만난다면 다음처럼 확인 요청해볼 일이다.

```text

이 Python 프로그램이 Arduino와 연결되지 않는 원인을 진단해줘.

1. Python 버전과 필요한 패키지를 확인하고 실제 오류를 재현해줘.

2. Windows에 표시되는 모든 COM 포트의 장치 설명을 확인해서

Bluetooth 가상 포트와 실제 USB 시리얼 포트를 구분해줘.

3. 포트가 한 번은 열리지만 두 번째부터 실패한다면

포트 점유, CH340 드라이버 버전, 장치 상태를 순서대로 점검해줘.

4. 수정 후에는 같은 포트를 두 번 연속 열어 명령 전송까지 검증해줘.

5. API 키는 코드에 넣지 말고 환경변수로 관리하며,

이름이 비슷한 기존 키와 충돌하는지도 확인해줘.

6. 오디오 감지는 실시간 RMS와 임계값을 출력해

실제 입력 신호가 들어오는지 눈으로 확인할 수 있게 해줘.

7. 모든 해결 내용을 진행상황 보고서에 원인·조치·검증 결과로 기록해줘.

```

## 다음 단계: 이제 진짜 바람만 남았다 🧭

IRF520 MOSFET 모듈이 도착하면 선풍기에 감정이입을 마무리할 예정이다.

1. Arduino PWM 출력과 MOSFET 신호 입력 연결

2. USB 선풍기 전원선과 MOSFET 출력부 연결

3. Arduino와 선풍기 전원의 공통 GND 구성

4. fan_speed(0~255) 값에 따른 실제 풍량 변화 검증

5. Gemini의 스트레스 판단과 음악 비트가 진짜 바람으로 이어지는 장면 촬영

한 가지 원칙은 분명하다. 선풍기 모터는 Arduino 출력 핀에 직접 연결하지 않는다. 모터는 전류를 많이 사용하므로 외부 전원을 쓰고 MOSFET이 스위치 역할을 맡아야 한다.

아직은 OLED가 웃고 LED가 깜빡이는 단계다. 하지만 텍스트 속 Gemini가 COM8을 건너 작은 화면에 표정을 짓고 음악에 반응하기 시작했다. 바람만 아직 없을 뿐, 선풍기의 영혼은 이미 들어오기 시작했다.

## 참고 자료

- Arduino Help Center: avrdude: ser_open(): can't set com-stat

1
2개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.