산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

이 튜토리얼에서는 아두이노 기반 무선 기상 관측소를 만드는 방법을 배웁니다. 아래 비디오를 시청하거나 작성된 튜토리얼을 읽어보세요.

개요

실외 온도와 습도는 DHT22 센서로 측정하고, 이 데이터를 NRF24L01 트랜시버 모듈을 통해 실내 유닛으로 무선 전송합니다. 실내 유닛에는 또 다른 DHT22 센서가 있어 실내 온도와 습도를 측정하며, DS3231 실시간 시계 모듈이 아두이노 전원이 꺼져도 시간을 유지합니다. 모든 데이터는 0.96인치 OLED 디스플레이에 출력됩니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

아두이노 무선 기상 관측소 회로도

회로도와 프로젝트 작동 원리를 살펴보겠습니다. 각 모듈의 자세한 작동 원리는 별도 튜토리얼에서 다루었으므로 참고하세요: NRF24L01 튜토리얼, DHT22 튜토리얼, DS3231 튜토리얼.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

이 프로젝트에 필요한 구성품은 아래 링크에서 구할 수 있습니다:

  • NRF24L01 트랜시버 모듈
  • DHT22 센서
  • DS3231 실시간 시계
  • 아두이노 나노

실시간 시계 모듈과 OLED 디스플레이는 모두 I2C 프로토콜로 아두이노와 통신하므로 아두이노 나노의 I2C 핀(아날로그 핀 4, 5)에 연결됩니다. NRF24L01 트랜시버 모듈 바로 옆에 커패시터를 두어 전원 공급을 더 안정적으로 만들었습니다. 또한 DHT22 데이터 핀에 풀업 저항을 연결해 센서가 제대로 작동하도록 했습니다.

전원 공급의 경우 실내 유닛에는 12V DC 어댑터를 사용했고, 실외 유닛에는 약 7.5V를 내는 리튬이온 배터리 두 개를 사용했습니다. 이 구성으로 실외 유닛은 데이터를 주기적으로 전송하고 그 사이에 아두이노를 슬립 모드(소비 전력 약 7mA)로 두어 약 10일간 작동할 수 있습니다.

커스텀 PCB 설계

회로도에 따라 전자 부품을 정리하기 위해 EasyEDA 무료 온라인 회로 설계 소프트웨어로 커스텀 PCB를 설계했습니다. 동일한 PCB를 실내/실외 유닛 모두에 사용할 수 있으며, 아두이노 보드만 다르게 프로그래밍하면 됩니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

설계를 마치면 PCB 제조용 거버 파일을 내보낼 수 있습니다. 이 프로젝트의 EasyEDA 프로젝트 파일은 여기서 확인하세요.

거버 파일을 JLCPCB(이 비디오의 후원사)에 주문합니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

거버 파일을 드래그 앤 드롭해 업로드하면 거버 뷰어에서 PCB를 검토할 수 있습니다. 모든 게 정상이면 원하는 PCB 속성을 선택하고 합리적인 가격에 주문할 수 있습니다. JLCPCB 첫 주문 시 최대 10개 PCB를 단 2달러에 받을 수 있습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

며칠 후 PCB가 도착했습니다. 품질이 훌륭하고 설계와 정확히 일치합니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

PCB에 핀 헤더를 납땜해 부품을 쉽게 연결/분리할 수 있게 했습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

커패시터와 풀업 저항도 납땜했습니다. 이제 부품을 PCB의 핀 헤더에 장착하기만 하면 됩니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

케이스 제작

프로젝트 케이스를 만들기 위해 8mm 두께의 MDF 보드를 사용했고, 원형 톱으로 모든 조각을 크기에 맞게 잘랐습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

정확한 온습도 측정을 위해 케이스 양쪽에 공기가 들어올 수 있는 슬롯을 뚫었습니다. 드릴과 줄을 사용해 실내/실외 유닛 양쪽 옆 패널에 여러 개의 슬롯을 만들었습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

전면 패널에는 OLED 디스플레이용 슬롯을 만들었고, 장식용으로 작은 알루미늄 조각을 잘라 전면 패널에 부착했습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

목재 접착제와 클램프, 나사로 케이스를 조립했습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

스프레이 페인트로 케이스를 도색했습니다. 실외 유닛은 흰색, 실내 유닛은 검은색을 사용했습니다. 페인트가 마른 후 PCB를 케이스에 넣었습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

실내 유닛 후면에 전원 잭과 스위치를, 실외 유닛에는 점퍼 와이어를 전원 스위치로 사용했습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

이제 아두이노 무선 기상 관측소가 작동합니다. 남은 건 프로그램이 어떻게 작동하는지 살펴보는 것입니다.

아두이노 무선 기상 관측소 코드

실외 유닛(송신기) 코드

/* 아두이노 무선 통신 튜토리얼 실외 유닛 - 송신기 by Dejan Nedelkovski, www.HowToMechatronics.com 라이브러리: NRF24L01 - TMRh20/RF24, https://github.com/tmrh20/RF24/ DHT22 - DHTlib, https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib LowPower - https://github.com/rocketscream/Low-Power */ 
#include <SPI.h> 
#include <nRF24L01.h> 
#include <RF24.h> 
#include <dht.h> 
#include <LowPower.h> 
#define dataPin 8 // DHT22 데이터 핀 
dht DHT; // DHT 객체 생성 
RF24 radio(10, 9); // CE, CSN 
const byte address[6] = "00001"; 
char thChar[32] = ""; 
String thString = ""; 
void setup() { 
  radio.begin(); 
  radio.openWritingPipe(address); 
  radio.setPALevel(RF24_PA_MIN); 
  radio.stopListening(); 
} 
void loop() { 
  int readData = DHT.read22(dataPin); // 센서에서 데이터 읽기 
  int t = DHT.temperature; // 온도 값 가져오기 
  int h = DHT.humidity; // 습도 값 가져오기 
  thString = String(t) + String(h); 
  thString.toCharArray(thChar, 12); // 문자 배열로 변환 
  // 실내 유닛으로 데이터 무선 전송 
  for (int i = 0; i <= 3; i++) { // 데이터 3회 전송 
    radio.write(&thChar, sizeof(thChar)); 
    delay(50); 
  } 
  // 2분간 슬립, 15*8 = 120초 
  for (int sleepCounter = 15; sleepCounter > 0; sleepCounter--) { 
    LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); 
  } 
}

설명: 실외 유닛은 무선 통신의 송신기입니다. RF24 라이브러리, DHT 라이브러리, 아두이노를 슬립 모드로 넣는 LowPower 라이브러리를 포함합니다. 인스턴스, 핀, 변수를 정의한 후 setup에서 무선 통신 주소를 초기화합니다. loop에서는 DHT22 센서에서 온도와 습도를 읽습니다. 이 값들은 처음에 정수이고 분리되어 있으므로 하나의 String 변수로 합치고 문자 배열에 넣어 radio.write() 함수로 실내 유닛에 전송합니다. for 루프로 데이터를 3번 전송해 수신기가 바쁠 때에도 데이터를 받을 수 있게 합니다. 마지막에 아두이노를 일정 시간 슬립 모드로 두어 전력 소모를 최소화합니다.

실내 유닛(수신기) 코드

/* 아두이노 무선 통신 튜토리얼 실내 유닛 - 수신기 by Dejan Nedelkovski, www.HowToMechatronics.com 라이브러리: DS3231 - https://www.rinkydinkelectronics.com/library.php?id=73 U8G2 - https://github.com/olikraus/u8g2 */ 
#include <SPI.h> 
#include <nRF24L01.h> 
#include <RF24.h> 
#include <dht.h> 
#include <DS3231.h> 
#include <U8g2lib.h> 
#include <Wire.h> 
#define dataPin 8 // DHT22 센서 
dht DHT; // DHT 객체 생성 
DS3231 rtc(SDA, SCL); 
U8G2_SSD1306_128X64_NONAME_1_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); 
RF24 radio(10, 9); // CE, CSN 
const byte address[6] = "00001"; 
char text[6] = ""; 
int readDHT22, t, h; 
String inTemp, inHum, outTemp, outHum; 
String rtcTime, rtcDate; 
int draw_state = 0; 
unsigned long previousMillis = 0; 
long interval = 3000; 
#define Temperature_20Icon_width 27 
#define Temperature_20Icon_height 47 
static const unsigned char Temperature_20Icon_bits[] U8X8_PROGMEM = { 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x80, 0x7f, 0x00, 0x00, 0xc0, 0xe1, 0x00, 0x00, 0xe0, 0xc0, 0x01, 0x00, 0x60, 0x80, 0xf9, 0x03, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x79, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0xf9, 0x03, 0x60, 0x80, 0x01, 0x00, 0x60, 0x8c, 0x79, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0xf9, 0x03, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x79, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0xf9, 0x03, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x70, 0x9e, 0x03, 0x00, 0x38, 0x1e, 0x07, 0x00, 0x18, 0x3e, 0x0e, 0x00, 0x1c, 0x3f, 0x0c, 0x00, 0x0c, 0x7f, 0x18, 0x00, 0x8c, 0xff, 0x18, 0x00, 0x8e, 0xff, 0x38, 0x00, 0xc6, 0xff, 0x31, 0x00, 0xc6, 0xff, 0x31, 0x00, 0xc6, 0xff, 0x31, 0x00, 0x8e, 0xff, 0x38, 0x00, 0x8c, 0xff, 0x18, 0x00, 0x0c, 0x7f, 0x1c, 0x00, 0x3c, 0x1c, 0x0e, 0x00, 0x78, 0x00, 0x06, 0x00, 0xe0, 0x80, 0x07, 0x00, 0xe0, 0xff, 0x03, 0x00, 0x80, 0xff, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 
}; 
#define Humidity_20Icon_width 27 
#define Humidity_20Icon_height 47 
static const unsigned char Humidity_20Icon_bits[] U8X8_PROGMEM = { 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0xdc, 0x00, 0x00, 0x00, 0xdc, 0x01, 0x00, 0x00, 0x8e, 0x01, 0x00, 0x00, 0x86, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x03, 0x07, 0x00, 0x80, 0x03, 0x06, 0x00, 0x80, 0x01, 0x0c, 0x00, 0xc0, 0x01, 0x1c, 0x00, 0xc0, 0x00, 0x18, 0x00, 0xe0, 0x00, 0x38, 0x00, 0x60, 0x00, 0x30, 0x00, 0x70, 0x00, 0x70, 0x00, 0x30, 0x00, 0xe0, 0x00, 0x38, 0x00, 0xc0, 0x00, 0x18, 0x00, 0xc0, 0x01, 0x1c, 0x00, 0x80, 0x01, 0x0c, 0x00, 0x80, 0x03, 0x0e, 0x00, 0x80, 0x03, 0x06, 0x00, 0x00, 0x03, 0x06, 0x00, 0x00, 0x03, 0x07, 0x00, 0x00, 0x07, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x03, 0x00, 0x00, 0x06, 0x63, 0x00, 0x00, 0x06, 0x63, 0x00, 0x00, 0x06, 0x63, 0x00, 0x00, 0x06, 0xe3, 0x00, 0x00, 0x06, 0xc7, 0x00, 0x00, 0x06, 0xc6, 0x01, 0x00, 0x07, 0x86, 0x03, 0x00, 0x03, 0x0e, 0x1f, 0x00, 0x03, 0x0e, 0x1e, 0x80, 0x01, 0x1c, 0x00, 0xc0, 0x01, 0x38, 0x00, 0xe0, 0x00, 0x78, 0x00, 0x70, 0x00, 0xf0, 0x00, 0x38, 0x00, 0xe0, 0x07, 0x1f, 0x00, 0x80, 0xff, 0x0f, 0x00, 0x00, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 
}; 
void setup() { 
  radio.begin(); 
  radio.openReadingPipe(0, address); 
  radio.setPALevel(RF24_PA_MIN); 
  radio.startListening(); 
  u8g2.begin(); 
  rtc.begin(); 
} 
void loop() { 
  if (radio.available()) { 
    radio.read(&text, sizeof(text)); // 수신 데이터 읽기 
    outTemp = String(text[0]) + String(text[1]) + char(176) + "C"; // 실외 온도 
    outHum = String(text[2]) + String(text[3]) + "%"; // 실외 습도 
  } 
  unsigned long currentMillis = millis(); 
  if (currentMillis - previousMillis > interval) { 
    previousMillis = currentMillis; 
    u8g2.firstPage(); 
    do { 
      switch (draw_state ) { 
        case 0: drawDate(); break; 
        case 1: drawInTemperature(); break; 
        case 2: drawInHumidity(); break; 
        case 3: drawOutTemperature(); break; 
        case 4: drawOutHumidity(); break; 
      } 
    } while ( u8g2.nextPage() ); 
    draw_state++; 
    if (draw_state > 4) { draw_state = 0; } 
  } 
} 
void drawDate() { 
  String dowa = rtc.getDOWStr(); 
  dowa.remove(3); 
  rtcDate = dowa + " " + rtc.getDateStr(); 
  u8g2.setFont(u8g2_font_timB14_tr); 
  u8g2.setCursor(0, 15); 
  rtcTime = rtc.getTimeStr(); // DS3231 RTC 시간 
  rtcTime.remove(5); 
  u8g2.print(rtcDate); 
  u8g2.setFont(u8g2_font_fub30_tf); 
  u8g2.setCursor(8, 58); 
  u8g2.print(rtcTime); 
} 
void drawInTemperature() { 
  readDHT22 = DHT.read22(dataPin); // 센서에서 데이터 읽기 
  t = DHT.temperature; // 온도 값 가져오기 
  inTemp = String(t) + char(176) + "C"; 
  u8g2.setFont(u8g2_font_helvR14_tr); 
  u8g2.setCursor(24, 15); 
  u8g2.print("INDOOR"); 
  u8g2.setFont(u8g2_font_fub30_tf); 
  u8g2.setCursor(36, 58); 
  u8g2.print(inTemp); 
  u8g2.drawXBMP( 0, 17, Temperature_20Icon_width, Temperature_20Icon_height, Temperature_20Icon_bits); 
} 
void drawInHumidity() { 
  h = DHT.humidity; // 습도 값 가져오기 
  inHum = String(h) + "%"; 
  u8g2.setFont(u8g2_font_helvR14_tr); 
  u8g2.setCursor(24, 15); 
  u8g2.print("INDOOR"); 
  u8g2.setFont(u8g2_font_fub30_tf); 
  u8g2.setCursor(36, 58); 
  u8g2.print(inHum); 
  u8g2.drawXBMP( 0, 17, Humidity_20Icon_width, Humidity_20Icon_height, Humidity_20Icon_bits); 
} 
void drawOutTemperature() { 
  u8g2.setFont(u8g2_font_helvR14_tr); 
  u8g2.setCursor(12, 15); 
  u8g2.print("OUTDOOR"); 
  u8g2.setFont(u8g2_font_fub30_tf); 
  u8g2.setCursor(36, 58); 
  u8g2.print(outTemp); 
  u8g2.drawXBMP( 0, 17, Temperature_20Icon_width, Temperature_20Icon_height, Temperature_20Icon_bits); 
} 
void drawOutHumidity() { 
  u8g2.setFont(u8g2_font_helvR14_tr); 
  u8g2.setCursor(12, 15); 
  u8g2.print("OUTDOOR"); 
  u8g2.setFont(u8g2_font_fub30_tf); 
  u8g2.setCursor(36, 58); 
  u8g2.print(outHum); 
  u8g2.drawXBMP( 0, 17, Humidity_20Icon_width, Humidity_20Icon_height, Humidity_20Icon_bits); 
}

설명: 실내 유닛(수신기)에서는 DS3231 실시간 시계 모듈용 라이브러리와 OLED 디스플레이용 U8G2 라이브러리 두 가지를 추가로 포함합니다. 앞서와 같은 방식으로 인스턴스, 핀, 프로그램에 필요한 변수들을 정의합니다. 또한 온도와 습도 아이콘을 비트맵으로 정의합니다.

온도 아이콘 비트맵:

#define Temperature_20Icon_width 27 
#define Temperature_20Icon_height 47 
static const unsigned char Temperature_20Icon_bits[] U8X8_PROGMEM = { 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x80, 0x7f, 0x00, 0x00, 0xc0, 0xe1, 0x00, 0x00, 0xe0, 0xc0, 0x01, 0x00, 0x60, 0x80, 0xf9, 0x03, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x79, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0x01, 0x00, 0x60, 0x80, 0xf9, 0x03, 0x60, 0x80, 0x01, 0x00, 0x60, 0x8c, 0x79, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0xf9, 0x03, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x79, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0xf9, 0x03, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x60, 0x9e, 0x01, 0x00, 0x70, 0x9e, 0x03, 0x00, 0x38, 0x1e, 0x07, 0x00, 0x18, 0x3e, 0x0e, 0x00, 0x1c, 0x3f, 0x0c, 0x00, 0x0c, 0x7f, 0x18, 0x00, 0x8c, 0xff, 0x18, 0x00, 0x8e, 0xff, 0x38, 0x00, 0xc6, 0xff, 0x31, 0x00, 0xc6, 0xff, 0x31, 0x00, 0xc6, 0xff, 0x31, 0x00, 0x8e, 0xff, 0x38, 0x00, 0x8c, 0xff, 0x18, 0x00, 0x0c, 0x7f, 0x1c, 0x00, 0x3c, 0x1c, 0x0e, 0x00, 0x78, 0x00, 0x06, 0x00, 0xe0, 0x80, 0x07, 0x00, 0xe0, 0xff, 0x03, 0x00, 0x80, 0xff, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 
};

GIMP 같은 오픈 소스 이미지 에디터로 그림을 그리고 비트맵(.xbm)으로 내보내면 됩니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

이 파일을 메모장으로 열면 비트맵 데이터를 복사해 아두이노 코드에 붙여넣을 수 있습니다.

아두이노 기반 무선 기상 관측소 만들기 - 안정적인 구축 가이드

PROGMEM 변수 수식어를 사용해 비트맵을 상수로 정의하면 플래시 메모리에 저장되어 SRAM을 절약할 수 있습니다.

static const unsigned char Temperature_20Icon_bits[] U8X8_PROGMEM // 플래시 메모리에 저장 
static unsigned char Temperature_20Icon_bits[] // SRAM에 저장

setup 섹션에서는 무선 통신, OLED 디스플레이, 실시간 시계 모듈을 초기화합니다.

loop 섹션에서는 NRF24L01 모듈을 통해 수신 데이터가 있는지 지속적으로 확인합니다. 데이터가 있으면 radio.read() 함수로 읽어 첫 두 문자는 온도 String 변수에, 다음 두 문자는 습도 String 변수에 저장합니다.

millis() 함수를 사용해 interval 변수(3초)로 설정된 간격으로 디스플레이에 다양한 데이터를 표시합니다. delay() 함수를 쓰면 프로그램이 대기하는 동안 실외 유닛의 수신 데이터를 놓칠 수 있으므로 millis()를 사용합니다.

U8G2 라이브러리의 firstPage()와 nextPage() 함수를 사용해 커스텀 함수로 정의된 5가지 화면을 출력합니다.

drawDate() 함수는 실시간 시계에서 날짜와 시간 정보를 가져와 디스플레이에 적절히 출력합니다. drawInTemperature() 함수는 실내 온도를 읽어 디스플레이에 출력합니다. 사실 같은 방식으로 모든 화면을 디스플레이에 출력합니다.

이 아두이노 프로젝트를 즐기셨고 새로운 것을 배우셨기를 바랍니다. 아래 댓글 섹션에서 자유롭게 질문해 주세요.

제조공정

  1. 라즈베리 파이 태양광 기상 관측소 DIY 제작
  2. 라즈베리 파이 2와 ADS-WS1로 야외 기상 관측소 구축하기
  3. 라즈베리 파이 기상 관측소 만들기: 매일 이메일로 날씨 데이터 전송
  4. 고급 기상 관측소 v2.0: 실시간 온도, 기압, 습도, 고도 모니터링
  5. 아두이노 오디오 주파수 감지기 - 고감도 사운드 모듈로 최대 음량 피크 주파수 측정
  6. 스마트 무선 조명 스위치 컨트롤러: 아두이노 기반 원격 스위치 조작기
  7. 아두이노 틱택토 게임 - MAX7219 LED 매트릭스와 골판지 케이스
  8. 스파크펀 7-세그먼트 디스플레이로 신뢰할 수 있는 아두이노 카운트다운 타이머 만들기
  9. 1만 원대 휴대용 아두이노 날씨 스테이션 – 나노, DHT22, BMP180, LCD로 직접 제작
  10. eDOT: 내장 IR 리모컨·자동 밝기 조절 갖춘 정밀 아두이노 시계·기상 관측소