아두이노 프로젝트 5. 스마트 자전거 헬멧: 두 판 사이의 차이

아두위키 : Arduwiki
편집 요약 없음
편집 요약 없음
 
(같은 사용자의 중간 판 7개는 보이지 않습니다)
3번째 줄: 3번째 줄:
== '''개요''' ==
== '''개요''' ==


=== 족저압 검사 깔창 ===
=== 스마트 자전거 헬멧 ===
압력 센서가 부착된 깔창에 올라서서 발바닥의 무게 중심을 확인하고 데이터를 수집하는 장치입니다.
원하는 방향으로 고개를 꺾으면 해당 방향에 네오픽셀이 켜지며 방향등의 역할을 하게 되는 스마트 자전거 헬멧입니다.


=== 고려사항 ===
=== 고려사항 ===


* 압력센서 8개를 깔창에 부착하고, 각각의 데이터를 수집합니다.
* 가속도 센서를 부착해 해당 데이터를 바탕으로 사람 고개 방향을 측정합니다.
* 깔창 위에 가만히 올라서있는 정적 검사, 깔창 위에서 자연스럽게 걸어서 검사하는 동적 검사, 무게 중심을 확인하는 평발 검사 모드를 구현합니다.
* 헬멧의 좌우에는 네오픽셀 스트랩이 부착되어 고개 방향에 따라 밝아져 방향등의 역할을 합니다.
* 유저 인터페이스를 통해 실시간으로 압력 센서가 눌리는 세기를 보여줍니다.
* 전원은 9V 배터리를 사용하며 쉽게 교체할 수 있으며 on/off 기능을 포함합니다.


=== '''사용 하드웨어''' ===
=== '''사용 하드웨어''' ===


* [https://gongzipsa.com/shop/1699939293 아두이노 나노] 2개
* [https://gongzipsa.com/shop/1699939462 아두이노 우노] 1개
* [https://gongzipsa.com/shop/1699939305 아두이노 메가]
* [[네오픽셀(WS2812)|네오픽셀]] 스트랩 2개
* RA30P 압력 센서 16개
* [https://gongzipsa.com/shop/1699939372 미니 브레드보드] 1개
* [https://gongzipsa.com/shop/1699939292 1K옴 저항 16개]
* [https://gongzipsa.com/shop/1699939460 MPU-6050] 가속도/자이로 센서 1개
* [[HM-10 블루투스 모듈]] 4개
* [https://gongzipsa.com/shop/1699939289 점퍼 케이블]
* [https://gongzipsa.com/shop/1699939289 점퍼 케이블]


24번째 줄: 23번째 줄:
== '''회로 구성''' ==
== '''회로 구성''' ==


=== 나노 파트 (같은 회로를 2개 구성해야 합니다.) ===
=== [[파일:자전거 헬멧 회로도.png|가운데|class=coders100]] ===
[[파일:족저압 회로 나노파트.jpg|가운데|class=coders100]]


=== 메가 파트 ===
[[파일:족저압 회로 메가파트.jpg|가운데|class=coders100]]


== '''기능 구현''' ==
== '''기능 구현''' ==
<syntaxhighlight lang="c++" line="1">
#include "Adafruit_NeoPixel.h"
#include <Wire.h>


=== 나노 파트 ===
#define neoPIN 12
<syntaxhighlight lang="c++" line="1">
#define NUMPIXELS 8
/*
#define bright 255
시리얼 이름 : BTSerial
#define dly 1000
baudrate : 115200 -> (AT+BAUD4)


왼발 연결 블루투스 모듈
Adafruit_NeoPixel neo(NUMPIXELS, neoPIN, NEO_GRB + NEO_KHZ800);
모듈 이름 : S_LEFT
핀번호 : 123456


오른발 연결 블루투스 모듈
////값을 변경해도 되는 변수//////////////////////////////////////////////////////////////////////////
모듈 이름 : S_RIGHT
//네오픽셀 점등 주기
핀번호 : 111111
const int blinkTIme = 500;
*/
#include <SoftwareSerial.h>
SoftwareSerial BTSerial(2, 3);                      //2(tx), 3(rx)
int Sensor[] = { A0, A1, A2, A3, A4, A5, A6, A7 }; //순서대로 1 ~ 8번 센서 사용 핀번호


unsigned char ValueSensor[9];
//아래 두 값은 좌측, 우측으로 얼마나 기울여야 불이 점등하는지 정하는 값이며 절대값이 클수록 많이 기울여야 합니다.
unsigned long ScanningTime = 30000; //단위 [ms]
const int left_limint = 20;
int dly = 100;                       //단위 [ms]
const int right_limint = -20;
////////////////////////////////////////////////////////////////////////////////////////////////////


void sensing_func_inner(int dy) {
const int MPU_addr = 0x68;
  for (int i = 0; i < 8; i++)
int16_t AcX, AcY, AcZ, Tmp, GyX, GyY, GyZ;
    ValueSensor[i] = map(analogRead(Sensor[i]), 0, 1024, 0, 254);
unsigned long past = 0;
  BTSerial.write(ValueSensor, 9);
float dt;
  delay(dy);
float accel_angel_x, accel_angel_y, accel_angel_z;
}
float gyro_angel_x, gyro_angel_y, gyro_angel_z;
float filtered_angel_x, filtered_angel_y, filtered_angel_z;
float baseAcX, baseAcY, baseAcZ;
float baseGyX, baseGyY, baseGyZ;
unsigned long t_now;
unsigned long t_prev;
bool neo_state[2] = { false, false };  //{우측, 좌측}


void setup() {
void setup() {
  neo.begin();
  neo.setBrightness(bright);
  neo.clear();
  neo.show();
   Serial.begin(9600);
   Serial.begin(9600);
   BTSerial.begin(9600);
   Serial.println("Seirla start");
   Serial.println("Serial start");
  initMPU6050();
   ValueSensor[8] = 255;
   calibAccelGyro();
  initDT();
   bool state2 = false;
  for (int i = 0; i < 6; i++) {
    if (!state2) {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 255, 0);
      }
    } else {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 0, 0);
      }
    }
    neo.show();
    state2 = !state2;
    delay(100);
  }
}
}


void loop() {
void loop() {
   sensing_func_inner(dly);
   readAccelGyro();
}
  calcDT();
 
  calcAccelYPR();
void value_check(){
  if (millis() - past > blinkTIme) {
  for(int i=0;i<8;i++){
    past = millis();
     Serial.print(ValueSensor[i]);
    Serial.println(accel_angel_y, 2);
     Serial.print("\t");
    if (accel_angel_y > right_limint) {  //우측
      if (neo_state[1]) {
        neo_state[1] = false;
        neo.clear();
      }
      if (!neo_state[0]) {
        for (int i = 0; i < NUMPIXELS / 2; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[0] = !neo_state[0];
      } else if (neo_state[0]) {
        neo.clear();
        neo_state[0] = !neo_state[0];
      }
     } else if (accel_angel_y < left_limint) {  //좌측
      if (neo_state[0]) {
        neo_state[0] = false;
      }
      if (!neo_state[1]) {
        for (int i = 4; i < NUMPIXELS; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[1] = !neo_state[1];
      } else if (neo_state[1]) {
        neo.clear();
        neo_state[1] = !neo_state[1];
      }
    } else {
      neo.clear();
     }
    neo.show();
   }
   }
  Serial.println();
}
}


void sensing_func(unsigned long ScanTime, int dy) {
void initMPU6050() {
   unsigned long StartTime = millis();
   Wire.begin();
   unsigned long EndTime = millis();
   Wire.beginTransmission(MPU_addr);
   while ((EndTime - StartTime) < ScanTime) {
   Wire.write(0x6B);
    sensing_func_inner(dy);
  Wire.write(0);
    EndTime = millis();
  Wire.endTransmission(true);
  }
}
}
</syntaxhighlight>
=== 메가 파트 ===
<syntaxhighlight lang="c++" line="1">
/*
공통사항
baudrate : 115200 -> (AT+BAUD4)
왼발 연결 블루투스 모듈
시리얼 번호 : Serial1
모듈 이름 : M_LEFT
핀번호 : 123456
오른발 연결 블루투스 모듈
시리얼 번호 : Serial2
모듈 이름 : M_RIGHT
핀번호 : 111111
*/


void setup() {
void readAccelGyro() {
   Serial.begin(9600);     //pc 시리얼
   Wire.beginTransmission(MPU_addr);
   Serial1.begin(9600); //블루투스 시리얼(왼발)
   Wire.write(0x3B);
   Serial2.begin(9600); //블루투스 시리얼(오른발)
  Wire.endTransmission(false);
   Serial.println("Serial start");
  Wire.requestFrom(MPU_addr, 14, true);
  AcX = Wire.read() << 8 | Wire.read();
   AcY = Wire.read() << 8 | Wire.read();
  AcZ = Wire.read() << 8 | Wire.read();
   Tmp = Wire.read() << 8 | Wire.read();
  GyX = Wire.read() << 8 | Wire.read();
  GyY = Wire.read() << 8 | Wire.read();
  GyZ = Wire.read() << 8 | Wire.read();
}
}


char idx1 = 0;
void calibAccelGyro() {
char idx2 = 0;
  float sumAcX = 0, sumAcY = 0, sumAcZ = 0;
  float sumGyX = 0, sumGyY = 0, sumGyZ = 0;


int left_val[100];
  readAccelGyro();
int right_val[100];
unsigned char t1;
unsigned char t2;


void loop() {
   for (int i = 0; i < 10; i++) {
   if (Serial1.available() > 0) {
    readAccelGyro();
    t1 = Serial1.read();
    sumAcX += AcX;
    if (t1 == 255) {
    sumAcY += AcY;
      idx1 = 0;
    sumAcZ += AcZ;
      for (int i = 0; i < 8; i++) {
     sumGyX += GyX;
        Serial.print(left_val[i]);
    sumGyY += GyY;
        Serial.print("\t");
    sumGyZ += GyZ;
      }
     delay(100);
      Serial.println("0");
     } else {
      left_val[idx1] = t1;
      idx1++;
     }
   }
   }
  baseAcX = sumAcX / 10;
  baseAcY = sumAcY / 10;
  baseAcZ = sumAcZ / 10;


   if (Serial2.available() > 0) {
   baseGyX = sumGyX / 10;
    t2 = Serial2.read();
  baseGyY = sumGyY / 10;
    if (t2 == 255) {
  baseGyZ = sumGyZ / 10;
      idx2 = 0;
      for (int i = 0; i < 8; i++) {
        Serial.print(right_val[i]);
        Serial.print("\t");
      }
      Serial.println("1");
    } else {
      right_val[idx2] = t2;
      idx2++;
    }
  }
}
}
</syntaxhighlight>


=== 블루투스 연결 ===
void initDT() {
[[HM-10 블루투스 모듈]]은 아두이노 나노 2개에 각각 하나씩, 메가에 2개까지 총 4개가 사용됩니다.
  t_prev = millis();
}


아두이노 나노 파트의 모듈들은 Slave 모드, 메가 파트의 모듈들은 Master 모드로 설정되어 통신합니다.
void calcDT() {
  t_now = millis();
  dt = (t_now - t_prev) / 1000.0;
  t_prev = t_now;
}


자세한 내용은 [[HM-10 블루투스 모듈]] 문서의 Master, Slave 모듈 연결 파트를 참고하세요.
void calcAccelYPR() {
  float accel_x, accel_y, accel_z;
  float accel_xz, accel_yz;
  const float RADIANS_TO_DEGREES = 180 / 3.14159;


  accel_x = AcX - baseAcX;
  accel_y = AcY - baseAcY;
  accel_z = AcZ + (16384 - baseAcZ);


'''Q.''' 압력센서 16개라면 굳이 블루투스 통신을 하지 않고 아두이노 메가 하나에서 작업해도 핀 개수가 충분하지 않나요?
  accel_yz = sqrt(pow(accel_y, 2) + pow(accel_z, 2));
  accel_angel_y = atan(-accel_x / accel_yz) * RADIANS_TO_DEGREES;


'''A.''' 처리 속도의 차이를 고려해 메가, 나노로 나누어 수집과 데이터 처리를 따로 처리했습니다.
  accel_xz = sqrt(pow(accel_x, 2) + pow(accel_z, 2));
 
  accel_angel_x = atan(accel_y / accel_xz) * RADIANS_TO_DEGREES;
아두이노 메가 하나로 모든 작업을 실행했을 때 처리 속도의 지연으로 인해 이후 인터페이스에서 빨간 점으로 표시되는 부분의 적용이
 
늦어지는 듯한 느낌을 받을 수 있습니다.


  accel_angel_z = 0;
}
</syntaxhighlight>


=== 데이터 수집 ===
아두이노 나노 파트에서 RA30P 압력센서(각 8개)의 데이터를 수집합니다.


나노 파트에서 수집된 데이터들은 블루투스 통신을 통해 메가 파트로 전송되며, 데이터 시각화 및 정적, 동적, 평발 검사 모드의 결과 계산을 위해 사용됩니다.
=== [[네오픽셀(WS2812)|네오픽셀]] ===
헬멧의 양 옆에는 네오픽셀 스트랩이 부착되어있습니다.


처음에는 정면을 바라본 상태로 전원을 켠 후 가속도 센서가 정상적으로 연결된다면 네오픽셀이 초록색으로 3번 깜빡거립니다.


=== 데이터 시각화 검사 인터페이스 제작 ===
만약 네오픽셀이 붉은 색으로 연속해서 점등한다면 에러가 발생한 상황으로 연결 확인 아두이노 리셋이 필요합니다.
Pygame 라이브러리를 활용해 압력센서 데이터 시각화 및 정적, 동적, 평발 검사 인터페이스를 제작합니다.


모습은 다음 사진과 같습니다.
이후에는 고개를 기울여 MPU-6050을 통해 수집된 데이터가 조건에 부합할 때 점등하며, blinkTime 변수에 선언된 값을 주기(ms 단위)로 깜빡거립니다.


[[파일:족저압 인터페이스.png|가운데|class=coders100]]
고개를 다시 똑바로 세우면 네오픽셀은 off 상태가 됩니다.




=== MPU-6050 가속도 센서 데이터 수집 ===
헬멧에 부착된 MPU-6050 가속도 센서를 통해 x, y, z축 가속도 데이터를 수집합니다.


압력센서 데이터에 따라 더 강한 압박을 받은 부분은 빨간 점이 더 크게, 약한 압박을 받은 부분은 더 작게 표현됩니다.
수집된 데이터를 원하는 값으로 가공한 후 left_limint, right_limint에서 미리 설정해 둔 임계값과 비교해 네오픽셀 점등 여부를 결정합니다.
 
좌측 상단 정적, 동적, 평발 검사 시작 버튼을 누르면 각 검사에 대한 파트가 나오게 됩니다.
 
* '''정적 검사''' : 일정 시간동안 깔창 위에 올라서서 가만히 서있는 사람의 압력센서 데이터를 수집하여 무게가 쏠리는 부위를 파악하고 이에 따른 결과(ex . 무게가 좌측, 우측, 전방, 후방으로 집중됩니다.)를 제공합니다.
* '''동적 검사''' : 일정 시간동안 깔창 위에서 제자리 걸음을 하는 사람의 압력센서 데이터를 수집하여 결과를 제공합니다.
* '''평발 검사''' : 일정 시간동안 깔창 위에 올라서서 가만히 서있는 사람의 압력센서 데이터를 수집하여 발의 아치 부분 압력을 파악하고 이에 따른 결과를 제공합니다.


left_limint, right_limint 값이 커질수록 고개를 더 많이 기울여야 작동합니다.




== '''결과''' ==
== '''결과''' ==


=== 정적 검사 ===
=== 전원 On 후 네오픽셀 초록색 점등 ===
[[파일:정적검사.png|가운데|class=coders100]]
[[파일:전원on.png|가운데|class=coders50]]
 
 
=== 동적 검사 ===
[[파일:동적검사.png|가운데|class=coders100]]




=== 평발 검사 ===
[[파일:평발검사.png|가운데|class=coders100]]


=== 좌우 기울임 ===
[[파일:스마트헬멧 완성사진.png|가운데|class=coders100]]




=== Comment ===
=== Comment ===
위에서부터 정적, 동적, 평발 검사 모습입니다.
사진 상으로 깜빡임이 표현되지 않지만 전원이 켜진 후 가속도 센서 연결이 확인되면 네오픽셀이 초록색으로 빠르게 3번 깜빡입니다.


손으로 압력센서를 누른 데이터를 기반으로 임의의 공식을 활용해 각 검사별 결과가 제공되는 모습입니다.
이후 좌우측 기울임에 따라 양쪽에 부착된 네오픽셀이 0.5초 간격으로 점등하는 것을 확인할 수 있습니다.





2024년 6월 12일 (수) 20:37 기준 최신판


개요

스마트 자전거 헬멧

원하는 방향으로 고개를 꺾으면 해당 방향에 네오픽셀이 켜지며 방향등의 역할을 하게 되는 스마트 자전거 헬멧입니다.

고려사항

  • 가속도 센서를 부착해 해당 데이터를 바탕으로 사람 고개 방향을 측정합니다.
  • 헬멧의 좌우에는 네오픽셀 스트랩이 부착되어 고개 방향에 따라 밝아져 방향등의 역할을 합니다.
  • 전원은 9V 배터리를 사용하며 쉽게 교체할 수 있으며 on/off 기능을 포함합니다.

사용 하드웨어


회로 구성

기능 구현

#include "Adafruit_NeoPixel.h"
#include <Wire.h>

#define neoPIN 12
#define NUMPIXELS 8
#define bright 255
#define dly 1000

Adafruit_NeoPixel neo(NUMPIXELS, neoPIN, NEO_GRB + NEO_KHZ800);

////값을 변경해도 되는 변수//////////////////////////////////////////////////////////////////////////
//네오픽셀 점등 주기
const int blinkTIme = 500;

//아래 두 값은 좌측, 우측으로 얼마나 기울여야 불이 점등하는지 정하는 값이며 절대값이 클수록 많이 기울여야 합니다.
const int left_limint = 20;
const int right_limint = -20;
////////////////////////////////////////////////////////////////////////////////////////////////////

const int MPU_addr = 0x68;
int16_t AcX, AcY, AcZ, Tmp, GyX, GyY, GyZ;
unsigned long past = 0;
float dt;
float accel_angel_x, accel_angel_y, accel_angel_z;
float gyro_angel_x, gyro_angel_y, gyro_angel_z;
float filtered_angel_x, filtered_angel_y, filtered_angel_z;
float baseAcX, baseAcY, baseAcZ;
float baseGyX, baseGyY, baseGyZ;
unsigned long t_now;
unsigned long t_prev;
bool neo_state[2] = { false, false };  //{우측, 좌측}

void setup() {
  neo.begin();
  neo.setBrightness(bright);
  neo.clear();
  neo.show();
  Serial.begin(9600);
  Serial.println("Seirla start");
  initMPU6050();
  calibAccelGyro();
  initDT();
  bool state2 = false;
  for (int i = 0; i < 6; i++) {
    if (!state2) {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 255, 0);
      }
    } else {
      for (int i = 0; i < NUMPIXELS; i++) {
        neo.setPixelColor(i, 0, 0, 0);
      }
    }
    neo.show();
    state2 = !state2;
    delay(100);
  }
}

void loop() {
  readAccelGyro();
  calcDT();
  calcAccelYPR();
  if (millis() - past > blinkTIme) {
    past = millis();
    Serial.println(accel_angel_y, 2);
    if (accel_angel_y > right_limint) {  //우측
      if (neo_state[1]) {
        neo_state[1] = false;
        neo.clear();
      }
      if (!neo_state[0]) {
        for (int i = 0; i < NUMPIXELS / 2; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[0] = !neo_state[0];
      } else if (neo_state[0]) {
        neo.clear();
        neo_state[0] = !neo_state[0];
      }
    } else if (accel_angel_y < left_limint) {  //좌측
      if (neo_state[0]) {
        neo_state[0] = false;
      }
      if (!neo_state[1]) {
        for (int i = 4; i < NUMPIXELS; i++) {
          neo.setPixelColor(i, bright, bright, 0);
        }
        neo_state[1] = !neo_state[1];
      } else if (neo_state[1]) {
        neo.clear();
        neo_state[1] = !neo_state[1];
      }
    } else {
      neo.clear();
    }
    neo.show();
  }
}

void initMPU6050() {
  Wire.begin();
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x6B);
  Wire.write(0);
  Wire.endTransmission(true);
}

void readAccelGyro() {
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr, 14, true);
  AcX = Wire.read() << 8 | Wire.read();
  AcY = Wire.read() << 8 | Wire.read();
  AcZ = Wire.read() << 8 | Wire.read();
  Tmp = Wire.read() << 8 | Wire.read();
  GyX = Wire.read() << 8 | Wire.read();
  GyY = Wire.read() << 8 | Wire.read();
  GyZ = Wire.read() << 8 | Wire.read();
}

void calibAccelGyro() {
  float sumAcX = 0, sumAcY = 0, sumAcZ = 0;
  float sumGyX = 0, sumGyY = 0, sumGyZ = 0;

  readAccelGyro();

  for (int i = 0; i < 10; i++) {
    readAccelGyro();
    sumAcX += AcX;
    sumAcY += AcY;
    sumAcZ += AcZ;
    sumGyX += GyX;
    sumGyY += GyY;
    sumGyZ += GyZ;
    delay(100);
  }
  baseAcX = sumAcX / 10;
  baseAcY = sumAcY / 10;
  baseAcZ = sumAcZ / 10;

  baseGyX = sumGyX / 10;
  baseGyY = sumGyY / 10;
  baseGyZ = sumGyZ / 10;
}

void initDT() {
  t_prev = millis();
}

void calcDT() {
  t_now = millis();
  dt = (t_now - t_prev) / 1000.0;
  t_prev = t_now;
}

void calcAccelYPR() {
  float accel_x, accel_y, accel_z;
  float accel_xz, accel_yz;
  const float RADIANS_TO_DEGREES = 180 / 3.14159;

  accel_x = AcX - baseAcX;
  accel_y = AcY - baseAcY;
  accel_z = AcZ + (16384 - baseAcZ);

  accel_yz = sqrt(pow(accel_y, 2) + pow(accel_z, 2));
  accel_angel_y = atan(-accel_x / accel_yz) * RADIANS_TO_DEGREES;

  accel_xz = sqrt(pow(accel_x, 2) + pow(accel_z, 2));
  accel_angel_x = atan(accel_y / accel_xz) * RADIANS_TO_DEGREES;

  accel_angel_z = 0;
}


네오픽셀

헬멧의 양 옆에는 네오픽셀 스트랩이 부착되어있습니다.

처음에는 정면을 바라본 상태로 전원을 켠 후 가속도 센서가 정상적으로 연결된다면 네오픽셀이 초록색으로 3번 깜빡거립니다.

만약 네오픽셀이 붉은 색으로 연속해서 점등한다면 에러가 발생한 상황으로 연결 확인 및 아두이노 리셋이 필요합니다.

이후에는 고개를 기울여 MPU-6050을 통해 수집된 데이터가 조건에 부합할 때 점등하며, blinkTime 변수에 선언된 값을 주기(ms 단위)로 깜빡거립니다.

고개를 다시 똑바로 세우면 네오픽셀은 off 상태가 됩니다.


MPU-6050 가속도 센서 데이터 수집

헬멧에 부착된 MPU-6050 가속도 센서를 통해 x, y, z축 가속도 데이터를 수집합니다.

수집된 데이터를 원하는 값으로 가공한 후 left_limint, right_limint에서 미리 설정해 둔 임계값과 비교해 네오픽셀 점등 여부를 결정합니다.

left_limint, right_limint 값이 커질수록 고개를 더 많이 기울여야 작동합니다.


결과

전원 On 후 네오픽셀 초록색 점등


좌우 기울임


Comment

사진 상으로 깜빡임이 표현되지 않지만 전원이 켜진 후 가속도 센서 연결이 확인되면 네오픽셀이 초록색으로 빠르게 3번 깜빡입니다.

이후 좌우측 기울임에 따라 양쪽에 부착된 네오픽셀이 0.5초 간격으로 점등하는 것을 확인할 수 있습니다.


추신

공집사의 아두이노 프로젝트 결과물은 판매되는 제품이 아니며, 프로젝트 수준에서 간단하게 진행되었습니다.

문의 및 의뢰는 크몽 공집사로 연락주시면 감사하겠습니다.