개발

[DIY] 르노삼성 SM3 Web 기반 리모컨 제어모듈 제작(코드 + 회로 포함)

미친개발 2026. 6. 13. 02:59
반응형

제작한 회로파일과 소스코드는 아래 링크에 있습니다.

* 필자가 직접 설계한 회로/코드이므로 상업적 이용 및 2차배포를 금지합니다.
* 잘 동작하는 것을 확인하였으나, 혹여나 따라하시다가 잘못되는 부분에 대해서 필자는 책임지지 않습니다.
https://github.com/koreankimchi123/wt_carkey

GitHub - koreankimchi123/wt_carkey: sm3용 원격(Web기반) 자동차키 개조용

sm3용 원격(Web기반) 자동차키 개조용. Contribute to koreankimchi123/wt_carkey development by creating an account on GitHub.

github.com

 

작동영상

https://youtube.com/shorts/9yC_NadAfKI?feature=share

 
 
 
요즘 날이 더워지고 있다
다른 차들은 휴대폰 앱으로 미리 에어컨을 틀어놓던데...
내차는 2018년식 SM3라 그런 기능이 없다.
 
그래서 직접 만들기로 했다.
 

1. 어떻게?

SM3에는 차 리모컨에 공조기를 틀 수 있는 버튼이 있다!

SM3 차키에는 에어컨, 히터를 틀 수 있는 버튼이 있다.

 
다만 이 기능을 쓰려면 차가 리모컨이 닿는 거리에 있어야 한다...
즉 나와 차가 멀리 떨어져있으면 미리 에어컨을 틀어놓을 수가 없다.
 
그런데 만약 내가 차와 멀리 떨어져있어도 차 리모컨 기능을 사용할 수 있다면?
웹 서버와 아두이노로 충분히 구현할 수 있지 않을까 하는 생각이 들었다.
 
차키를 차 안에 두고, 스마트폰(웹)으로 신호를 주면 그 차키를 아두이노가 대신 눌러주는 원리로 구현할 수 있을 것 같아서 만들어보기로 했다.
 

2. 준비물

- WT32-ETH01 1개
- AQY210EHA 릴레이 4개
- 2N2222 트랜지스터 1개
- DS1E-S-DC5V 릴레이 1개
- 1N4007 다이오드 1개
- 390옴 저항 4개
- 1k옴 저항 1개
- 전원 공급용 USB 모듈 1개
- 르노 SM3 자동차 키 회로(차키를 아예 뜯어야 하므로 예비키 사용 추천)
- E5885 라우터 (+ 유심)
 

3. 회로 제작

회로도는 아래와 같다.

 
회로도 상 제일 왼쪽에 있는 J2 remote_button 1~8중 1,2는 SM3의 잠금버튼, 3,4는 열림버튼, 5,6은 에어컨버튼, 7,8은 충전구 버튼에 연결하게 된다. WT32-ETH01(아두이노)이 각 릴레이로 신호를 보내면 릴레이를 통해 해당 버튼 양단의 접점이 연결되는 효과를 주어 마치 버튼이 눌린 것으로 인식시킬 수 있다. 즉 WT32-ETH01을 통해 차 키를 자동으로 누를 수 있다.
 
버튼을 누를 때는 소형 Photomos 릴레이인 AQY210EHA를 사용해서 하는데, 트랜지스터를 사용하지 않는 이유는 SM3 리모컨의 버튼이 아주 미세한 누설전류만으로도 눌린 것으로 인식할 수 있기 때문에 최대 누설전류가 1μA 미만인 AQY210EHA를 이용한다.
 
J1 remote_pwr 부분은 SM3 리모컨 전원부(3v 코인셀 배터리가 들어가는 부분 +,-)에 연결하고, 이 부분에는 DS1E-S-DC5V 릴레이를 사용하는데, 이건 리모컨에 공급되는 3v 전원을 통제하기 위해서이다. 
SM3는 차키가 차 안에 있으면 차 문을 열 수 있기도 하고, 문만 열려있으면 시동을 마음대로 걸 수 있기 때문에 차를 잠굴 때는 차 내부에서 차 키의 존재가 인식되면 안된다. 따라서 잠금을 한 뒤에는 차 리모컨의 전원을 주지 않고, 열림을 한 뒤에는 차 리모컨의 전원을 유지해야 하기 때문에 리모컨 전원공급의 통제를 위한 릴레이가 필요하다. 
이 때 릴레이는 내부 코일 구조로 인해 역전압(플라이백)이 튀어 자동차 퓨즈가 터지는 경우가 생길 수도 있기 때문에 역전압 방지를 위한 다이오드를 추가하여 준다. 또한 아두이노 GPIO는 신호용이기 때문에 DC5V 릴레이를 직접적으로 작동하지 못하므로 릴레이는 npn 트랜지스터(2n2222)를 통해 외부 5v 전원을 받아 제어한다.
 
위 회로대로 PCB 기반을 제작한다.
회로는 KiCad를 통해 제작하였고
제작된 기판은 아래와 같다.
 

 
위에서 말한 J2 remote_button 1~8이 rmt_btn의 좌측에 있는 1번부터 우측 8번까지 핀에 해당하고, 
J1 remote_pwr이 rmt_pwr에 해당한다. 
 
좌측 하단에 위치한 5v 전원은 usb 모듈을 연결하여 전원공급용으로 사용한다.
 
이 회로대로 gbr 파일을 만들어 JLCPCB에 회로를 주문한다.
 

 
 

 
 
필요한 부품들까지 전부 납땜을 하면 다음과 같다.
 
다음으로 이 회로에 차키를 연결해줘야 한다.
 

 
차 키를 뜯기가 굉장히 어렵다..
옆면이 다 붙어있어서 칼로 긁으면서 직접 갈라줘야 한다.
즉 한 번 분해하면 다시 사용하지 못하는 구조이므로 예비용 키를 사용하는걸 권장한다.
 

 
다 뜯으면 이런 모습이다.
 
이제 여기서 각 버튼들의 양단과 전원부(배터리 닿는 부분)에 선을 연결(납땜)해준다.
 

 
 
최종적으로 회로와 연결된 형태는 아래와 같다.
 

 

4. WT32-ETH01 아두이노 코딩

WT32-ETH01은 ESP32 기반의 랜선을 연결할 수 있는 IOT 보드이다.
즉 아두이노를 통해 코딩을 하고, LAN 통신을 할 수 있다.
물론 일반 ESP32를 이용해 Wifi 통신을 할 수도 있지만, 전력소모나 연결안정성을 고려했을 때 라우터와 LAN을 통해 연결하는 것이 좋다 판단하여 WT32-ETH01 보드를 사용했다.
 
아래는 MQTT 기반 통신을 하는 코드이다.
코드에서 MQTT_SERVER과 MQTT_PORT 값은 자신의 서버 주소와 포트로 바꿔줘야 한다.
코드 업로드는 CH340같은 USB to TTL 시리얼 어댑터를 통해 업로드하면 된다.
 

/*
 * WT32-ETH01 차키 원격 제어 (저전력: WiFi/BT OFF + 동적(수동) 클럭 스케일링)
 *
 * 적용 내용:
 * 1) WiFi/BT 완전 OFF (LAN만 사용)
 * 2) 동적 클럭(수동 DFS): 평소 80MHz, 명령 처리/재연결 동안만 240MHz 부스트
 * 3) 3시간마다 서버로 더미 패킷 전송
 *
 * 핀 연결:
 * - GPIO 17: 문 열기 버튼
 * - GPIO 33: 문 잠금 버튼
 * - GPIO 14: 에어컨 버튼 (3초 이상)
 * - GPIO 4:  주유구 버튼
 * - GPIO 32: 리모컨 전원 스위치 (HIGH = ON)
 *
 * 동작 순서:
 * 1. 전원 ON (GPIO 32 HIGH)
 * 2. 버튼 누르기
 * 3. 버튼 떼기
 * 4. 전원 OFF (문 열기 제외)
 */

#include <ETH.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include "esp_bt.h"

// ===== 설정 =====
const char* MQTT_SERVER = "자신의 서버 IP 입력";
const int   MQTT_PORT   = "연 서버 포트 입력";

// 핀 정의
#define PIN_UNLOCK 17  // 문 열기
#define PIN_LOCK   33  // 문 잠금
#define PIN_AIRCON 14  // 에어컨
#define PIN_FUEL   4   // 주유구
#define PIN_POWER  32  // 리모컨 전원

// 버튼 누르는 시간 (ms)
#define PRESS_TIME_NORMAL 500   // 일반 버튼
#define PRESS_TIME_AIRCON 3500  // 에어컨 (3.5초)

// ===== 더미 패킷 / Keepalive =====
// 3시간마다 서버로 더미 패킷 전송
#define DUMMY_INTERVAL_MS (3UL * 60UL * 60UL * 1000UL)
unsigned long lastDummySent = 0;

// ===== 전역 변수 =====
WiFiClient ethClient;
PubSubClient mqtt(ethClient);

bool ethConnected = false;
bool powerKeepOn  = false;  // 문 열기 후 전원 유지 플래그

// ===== 저전력: 수동 DFS =====
static inline void cpuIdleLow()  { setCpuFrequencyMhz(80);  }   // 평소
static inline void cpuBoostHigh(){ setCpuFrequencyMhz(240); }   // 순간 부스트

static inline void disableWifiBt() {
  // LAN(ETH)만 사용할 것이므로 WiFi/BT 완전 OFF
  WiFi.mode(WIFI_OFF);
  btStop();
}

// ETH 이벤트 핸들러
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      Serial.println("ETH 시작");
      ETH.setHostname("wt32-carkey");
      break;

    case ARDUINO_EVENT_ETH_CONNECTED:
      Serial.println("ETH 연결됨");
      break;

    case ARDUINO_EVENT_ETH_GOT_IP:
      Serial.print("ETH IP: ");
      Serial.println(ETH.localIP());
      ethConnected = true;
      break;

    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Serial.println("ETH 연결 해제");
      ethConnected = false;
      break;

    default:
      break;
  }
}

// ===== 버튼 동작 함수들 =====
void powerOn() {
  digitalWrite(PIN_POWER, HIGH);
  delay(100);  // 전원 안정화 대기
  Serial.println("전원 ON");
}

void powerOff() {
  if (!powerKeepOn) {
    digitalWrite(PIN_POWER, LOW);
    Serial.println("전원 OFF");
  } else {
    Serial.println("전원 유지 (unlock 상태)");
  }
}

void pressButton(int pin, int duration) {
  // Active LOW라고 써있는데 기존 코드가 HIGH=눌림 이라서 그대로 유지합니다.
  // 실제 하드웨어가 LOW=눌림이면 아래 두 줄을 서로 바꾸세요.
  digitalWrite(pin, HIGH);  // 버튼 누름
  delay(duration);
  digitalWrite(pin, LOW);   // 버튼 뗌
}

// 문 열기 - 전원 유지
void executeUnlock() {
  Serial.println(">> 문 열기 실행");

  powerOn();
  pressButton(PIN_UNLOCK, PRESS_TIME_NORMAL);

  powerKeepOn = true; // 잠금 누를 때까지 유지

  mqtt.publish("carkey/ack", "unlock:ok");
  Serial.println(">> 문 열기 완료 (전원 유지)");
}

// 문 잠금 - 전원 차단
void executeLock() {
  Serial.println(">> 문 잠금 실행");

  if (!powerKeepOn) {
    powerOn();
  }

  pressButton(PIN_LOCK, PRESS_TIME_NORMAL);

  powerKeepOn = false;
  powerOff();

  mqtt.publish("carkey/ack", "lock:ok");
  Serial.println(">> 문 잠금 완료 (전원 OFF)");
}

// 에어컨 - 3초 이상 누르기
void executeAircon() {
  Serial.println(">> 에어컨 실행 (3.5초)");

  powerOn();
  pressButton(PIN_AIRCON, PRESS_TIME_AIRCON);
  powerOff();

  mqtt.publish("carkey/ack", "aircon:ok");
  Serial.println(">> 에어컨 완료");
}

// 주유구
void executeFuel() {
  Serial.println(">> 주유구 실행");

  powerOn();
  pressButton(PIN_FUEL, PRESS_TIME_NORMAL);
  powerOff();

  mqtt.publish("carkey/ack", "fuel:ok");
  Serial.println(">> 주유구 완료");
}

// ===== 3시간마다 더미 패킷 전송 =====
void sendDummyPacket() {
  if (!mqtt.connected()) return;

  cpuBoostHigh();

  String payload = "dummy:";
  payload += String(millis());

  mqtt.publish("carkey/dummy", payload.c_str(), false);

  Serial.print("더미 패킷 전송: ");
  Serial.println(payload);

  cpuIdleLow();
}

// MQTT 메시지 수신 콜백
void mqttCallback(char* topic, byte* payload, unsigned int length) {
  cpuBoostHigh(); // 명령 처리 동안만 부스트

  String message;
  message.reserve(length);
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }

  Serial.print("명령 수신: ");
  Serial.println(message);

  if (message == "unlock") {
    executeUnlock();
  } else if (message == "lock") {
    executeLock();
  } else if (message == "aircon") {
    executeAircon();
  } else if (message == "fuel") {
    executeFuel();
  }

  cpuIdleLow(); // 처리 끝나면 저클럭 복귀
}

// MQTT 재연결
void reconnectMQTT() {
  cpuBoostHigh(); // 재연결 동안 부스트

  while (!mqtt.connected()) {
    Serial.print("MQTT 연결 중...");

    // 클라이언트 ID, Last Will 설정
    if (mqtt.connect("wt32-carkey", NULL, NULL, "carkey/status", 1, true, "offline")) {
      Serial.println("연결됨!");

      mqtt.publish("carkey/status", "online", true);
      mqtt.subscribe("carkey/command");

      // MQTT 재연결 직후 기준으로 더미 패킷 타이머 초기화
      lastDummySent = millis();

      break;
    } else {
      Serial.print("실패, rc=");
      Serial.print(mqtt.state());
      Serial.println(" 5초 후 재시도...");
      delay(5000);
    }
  }

  cpuIdleLow(); // 끝나면 저클럭 복귀
}

void setup() {
  Serial.begin(115200);
  Serial.println("\n=== WT32-ETH01 차키 컨트롤러 (저전력) ===");

  // 기본 클럭을 80MHz로 시작
  cpuIdleLow();

  // WiFi/BT OFF (ETH만 사용)
  disableWifiBt();

  // 핀 설정
  pinMode(PIN_UNLOCK, OUTPUT);
  pinMode(PIN_LOCK, OUTPUT);
  pinMode(PIN_AIRCON, OUTPUT);
  pinMode(PIN_FUEL, OUTPUT);
  pinMode(PIN_POWER, OUTPUT);

  // 초기 상태: 버튼 모두 LOW (눌리지 않음)
  digitalWrite(PIN_UNLOCK, LOW);
  digitalWrite(PIN_LOCK, LOW);
  digitalWrite(PIN_AIRCON, LOW);
  digitalWrite(PIN_FUEL, LOW);
  digitalWrite(PIN_POWER, LOW);

  // 이더넷 초기화
  WiFi.onEvent(WiFiEvent);
  ETH.begin();

  // MQTT 설정
  mqtt.setServer(MQTT_SERVER, MQTT_PORT);
  mqtt.setCallback(mqttCallback);

  Serial.println("초기화 완료");
}

void loop() {
  if (ethConnected) {
    if (!mqtt.connected()) {
      reconnectMQTT();
    }

    mqtt.loop();

    // 3시간마다 더미 패킷 전송
    unsigned long now = millis();
    if (mqtt.connected() && now - lastDummySent >= DUMMY_INTERVAL_MS) {
      lastDummySent = now;
      sendDummyPacket();
    }
  }

  delay(10);
}

 
 

5. nodeJS 서버 열기

스마트폰을 통해 WT32-ETH01에 신호를 보내려면 중간에 스마트폰 웹으로부터 신호를 받고 MQTT 통신을 전달해주는 서버가 필요하다. nodeJS기반으로 제작한 서버를 미니PC에서 24시간 돌려놓으면 된다. (물론 공유기에서 포트는 열어놓아야 한다.)
 
제작한 코드는 github에 올려두었다.
자세한 사항은 README.md 참조
https://github.com/koreankimchi123/wt_carkey

GitHub - koreankimchi123/wt_carkey: sm3용 원격(Web기반) 자동차키 개조용

sm3용 원격(Web기반) 자동차키 개조용. Contribute to koreankimchi123/wt_carkey development by creating an account on GitHub.

github.com

 

 
서버를 열고 스마트폰 브라우저를 통해 해당 포트로 접속하면 차 키를 조작할 수 있는 화면을 볼 수 있다.
해당 웹 페이지는 PWA가 적용되어 홈화면에 추가하면 앱처럼 사용할 수도 있다.
 

6. 차에 장착하기

LAN케이블을 뽑을 수 있는 휴대용 라우터를 연결하고, 차량에 장착한다.
필자는 E5885 라우터를 사용하였다.
 

 
다음과 같이 연결하고, 라우터를 자동차 퓨즈박스에서 뽑아온 상시전원에 물려 회로와 함께 24시간 가동될 수 있도록 한다.
 

7. 작동 테스트

https://youtube.com/shorts/9yC_NadAfKI?feature=share

 
 
잘된다야호
 
이걸로 이제 집 나오기 전에 차에 에어컨 틀어놓으면 된다.

반응형

'개발' 카테고리의 다른 글

git 명령어 정리  (1) 2024.12.31
Docker 명령어 정리  (0) 2024.12.31