May 15, 2024

디바이스마트 미디어:

[66호] 원하는 색상으로 제어가 가능한 아두이노 IoT 스마트 무드등 키트 -

2021-06-25

★2021 ICT 융합 프로젝트 공모전 결과 발표! -

2021-05-12

디바이스마트 국내 온라인 유통사 유일 벨로다인 라이다 공급! -

2021-02-16

★총 상금 500만원 /2021 ICT 융합 프로젝트 공모전★ -

2021-01-18

디바이스마트 온라인 매거진 전자책(PDF)이 무료! -

2020-09-29

[61호]음성으로 제어하는 간접등 만들기 -

2020-08-26

디바이스마트 자체제작 코딩키트 ‘코딩 도담도담’ 출시 -

2020-08-10

GGM AC모터 대량등록! -

2020-07-10

[60호]초소형 레이더 MDR, 어떻게 제어하고 활용하나 -

2020-06-30

[60호]NANO 33 IoT보드를 활용한 블루투스 수평계 만들기 -

2020-06-30

라즈베리파이3가 드디어 출시!!! (Now Raspberry Pi 3 is Coming!!) -

2016-02-29

MoonWalker Actuator 판매개시!! -

2015-08-27

디바이스마트 레이저가공, 밀링, 선반, 라우터 등 커스텀서비스 견적요청 방법 설명동영상 입니다. -

2015-06-09

디바이스마트와 인텔®이 함께하는 IoT 경진대회! -

2015-05-19

드디어 adafruit도 디바이스마트에서 쉽고 저렴하게 !! -

2015-03-25

[29호] Intel Edison Review -

2015-03-10

Pololu 공식 Distributor 디바이스마트, Pololu 상품 판매 개시!! -

2015-03-09

[칩센]블루투스 전 제품 10%가격할인!! -

2015-02-02

[Arduino]Uno(R3) 구입시 37종 센서키트 할인이벤트!! -

2015-02-02

[M.A.I]Ahram_ISP_V1.5 60개 한정수량 할인이벤트!! -

2015-02-02

[66호] 작업 현장을 위한 새로운 솔루션, 터프시스템2.0

디월트 1

디월트 1

디월트 2

 

 

 

 

 

 

 

 

디월트

작업 현장을 위한 새로운 솔루션, 터프시스템2.0

디월트는 수십여년간 뛰어난 품질과 내구성으로 많은 사람들이 신뢰하는 공구 브랜드이다. 디월트의 터프시스템 2.0은 용도에 맞게 체결하여 사용할 수 있는 모듈형 시스템 솔루션으로 IP65 단계의 방진/방습 기능을 적용해 먼지와 물로부터 제품을 보호할 수 있게 제작했으며,  외부 충격에 강한 곡선 디자인을 추가했다. 내구성 강한 재질, 터프시스템 1.0과 비교 했을 때 약 20% 넓어진 저장 공간, 탈부착 핸들, 손쉽게 적재/이동이 가능하다는 점 등 직관적이고 편리한 사용으로 건설부터 자동차, 목공, 금속 가공 및 전기 공사에 이르기까지, 전문가들이 현장에서 원활히 작업할 수 있다.

디월트의 신제품 터프시스템 2.0의 가장 큰 장점은 해외에서 특허 획득한 원터치 자동 체결/잠금 장치이다. 사용자가 가볍게 눌러주면 자동 체결 및 분리되어 현장 상황과 용도에 맞게 적재 모듈을 선택할 수 있어 터프시스템의 장점을 최대로 이끌어냈다. 또한 8인치 PU(폴리우레탄)휠을 추가 적용해 이동성 또한 높였고, 기존 터프시스템 1.0 제품과 호환도 가능해 사용 편의성을 증대했다. 제품 손상 및 흔들림을 최소화할 수 있게끔 견고하게 제작된 디월트 터프시스템 2.0 은 소형, 중형, 대형, 이동식 공구박스, 3 in 1 모듈 등 총 5종으로 지금 바로 디바이스마트에서 구매할 수 있다.

 

작업 현장을 위한 새로운 솔루션, 터프시스템2.0 제품 바로가기

[66호] 빛 하나로 살균 걱정 끝, UVON UVC 살균 LED 출시!

UVON

 

UVON

 

UVON

빛 하나로 살균 걱정 끝,  UVON UVC 살균 LED 출시!

UVON은 첨단 소재 기술 중심의 반도체 부품 전문 제조업체 포인트엔지니어링이 2019년 성공적으로 론칭한 UV LED 브랜드이다. UVON의 제품들은 수직절연층으로 이루어진 알루미늄 기판에 포인트 엔지니어링의 고유 기술인 특수 표면처리 기술이 적용된 기판을 적용했다. 일본의 DOWA Electronics의 기술 협업을 통해 개발됐으며 기존 세라믹 기판 대비 방열 및 반사도가 뛰어나 100mW 이상의 고출력이 가능하다. UVON의 대표 제품인 UV LED는 UVA,UVB, UVC가 있다. UV 광선이란 자외선 (Ultra Violet: UV)을 뜻하며 397~10nm에 이르는 파장으로 이뤄진 전자기파를 일컫는다. UV는 흔히 체내 비타민D를 합성하고 살균 작용을 하는 등의 화학작용을 한다고 알려져 있다. 그중에서도 UVA는 315~400nm의 장파장으로 인체영향이 상대적으로 낮아 선탠 기계나 의료장비에 많이 사용되고 있다. 다음으로 UVB는 2870~315nm의 중 파장으로 UVA와 같이 선탠 혹은 의료기기에 사용되지만 인체에 장시간 노출 시 피부 DNA를 손상시킬 수 있는 위험이 있어 사용에 유의해야 한다. 마지막으로 UVC는 100~280nm의 단파 장을 가진 광선으로 살균효과가 뛰어나 병원, 비행기, 버스, 지하철 및 식수 소독에 활용되며 시판되고 있는 UV 살균기의 대부분에 적용되고 있다. 다만 인체에 직접 가했을 때는 위험할 수 있으니 피해야 한다. 이 3가지 UV광선 LED 중 디바이스마트에서는 살균력이 가장 뛰어난 UVC LED 4종을 먼저 만나볼 수 있다. 살균 소독이 필수가 되어버린 요즘 UVON UVC LED로 예방해보자

 

UVON UVC 살균 LED 제품 바로가기

[66호] 작지만 강한 라즈베리파이 피코 (Raspberry Pi Pico)

라즈베리파이피코

라즈베리파이피코

 

 

RASPBERRY-PI

작지만 강한 라즈베리파이 피코 (Raspberry Pi Pico)

라즈베리파이 피코는 자체 설계한 RP2040 프로세서를 탑재한 마이크로컨트롤러(MCU) 이다. 처리속도가 133MHz으로 마이크로컨트롤러치고 빠른 속도를 가졌으며 Cortex-M0+ 을 사용하는 대표적인 아두이노, Arduino MKR WiFi 1010과 비교해보자면 WiFi1010이 48MHz로 속도로 피코는 이보다 약 2.7 배 정도 빠르다. 칩셋에 264킬로바이트(KB)의 자체 RAM과, 보드에 2메가바이트(MB)의 플래시 메모리를 내장했고, QSPI 버스를 이용해 16메가바이트(MB) 외장 플래시메모리를 지원한다. ARM 계열 프로세서답게 I2C,SPI 2개, UART 3개, 16채널 PWM, USB 1.1 호스트까지 다양한 입출력 옵션을 제공하고 드래그 & 드랍식으로 USB를 사용하여 플래시 메모 리에 프로그래밍이 가능하기 때문에 라즈베리파이 피코의 크기가 매우 작음에도 뛰어난 가성비를 바탕으로 다양하고 무한한 프로젝트를 완성할 수 있다. 라즈베리파이 피코는 C /C + + 또는 MicroPython을 시작하기 위한 라이브러리뿐만 아니라 마이크로 컨트롤러로 첫걸음을 내딛거나 RP2040을 사용하여 제품을 개발하 려는 경우 필요한 자체 영문 가이드를 제공 하고 있다. 디바이스마트는 라즈베리파이 피코를 더 많은 사람들이 진입장벽 없이 대중적으로 사용할 수 있게끔 자체 한글화 자료를 제공할 예정이다. 라즈베리파이 피코의 자세한 사양은 디바이스마트 상세페이지를 통해 확인할 수 있다.

 

제품사양

· 21mm × 51mm 폼팩터

· 1.8 ~ 5.5V DC 입력 전압

· 작동온도 -20°C ~ +85°C

· 온도 센서 내장

· 부동소수점 라이브러리 내장(on-chip)

 

작지만 강한 라즈베리파이 피코  (Raspberry Pi Pico) 제품 바로가기

[66호]20대 주거환경을 위한 외·내부 환기 제어 시스템

66_ict_환기제어 (1)

66_ict_환기제어 (1)

2020 ICT 융합 프로젝트 공모전 장려상

20대 주거환경을 위한 외·내부 환기 제어 시스템

글 | 항공대학교 이종민

1. 심사평
칩센 우선 보고서를 통하여 작품에 대하여 조금 더 설명해주었으면 좋았겠다는 생각은 들지만, 주제로 삼은 작품명의 “20대 주거 환경을 위한~”으로 시작하는 부분이 매우 인상적입니다. 공기 청정이 필요한 이유를 단순히 외부에서 발현한 공기질의 저하가 아닌, 20대가 사는 주거환경에서는 왠지 실내의 공기질이 더 나쁠 수도 있겠다는 생각이 들었습니다. 작품명을 아주 잘 정하신거 같습니다. 그리고 다른 유사 작품들과의 차이점, 두 개의 장치(실내/외)가 서로 통신하여 연동하도록 한 부분은 매우 기발한 발상으로 보입니다. 스마트폰 또는 인터넷을 통해 추출된 지역의 정보보다는 실제로 필요한 것은 내가 머무르고 있는 실내 공간과 외부 공간의 공기질의 차이가 더 중요하다는 생각이 들기 때문입니다. 전반적으로 데모 작품이 구성이나 제작이 매우 깔끔하게 된 듯 합니다.

펌테크 실생활과 접목된 실용성을 지닌 작품으로 생각됩니다. 전체적으로 꼼꼼하게 잘 기획되었고, 간결하게 잘 구성한 완성도 높은 작품이라고 생각합니다.

위드로봇 아이디어가 좀 더 추가되면 좋은 작품이 될 것 같습니다.

2. 배경

66_ict_환기제어 (2)

미세먼지 농도가 심각하다는 것은 아마 우리 모두가 알고 있을 것이다. 그리고 실제로 미세먼지로 인한 두통, 기침, 폐암, 그리고 직업 능률 감소로까지 이어지고 있다. 네이버 키워드 검색량을 확인해보면 미세먼지라는 것이 큰 문제이고 많은 사람들이 관심을 가지고 있다는 것을 확인할 수 있다.

2.1. 현재상황

66_ict_환기제어 (3)
미세먼지 문제를 해결하기위한 공기기청정기는 2018년 기준으로 2조원 시장을 가지고 있고 미니 공기청정기 시장은 약 1000억원의 시장을 형성하고 있다. 하지만 미니 공기청정기는 단지 필터와 펜으로 이루어진 공기청정기로 사람들이 어느정도 공기가 오염되었는지 확인할 수 없다. 또한 미니 공기청정기를 분해해보면 공기청정기의 필터가 역할을 제대로 하지 못하고 펜이 약한 경우가 대부분이다. 배경에서 말했듯이 대부분의 사람들은 초미세먼지로 인해 창문을 열고 살지 않고 있다 하지만 기사에 따르면 집안의 먼지농도와 밖의 미세먼지 농도를 비교 했을 때 밖의 미세먼지 농도가 높다고 한다. 이러한 상황에는 공기청정기를 계속 틀기보다 창문을 여는 것이 더 효과적인 방법이 될 수 있다. 그래서 저는 이 두가지 문제점을 해결하기 위한 제품을 만들려고 했다.

66_ict_환기제어 (4)

2.2. 제품의 특징
집안의 미세먼지 농도를 확인할 수 있고 내부의 미세먼지가 외부보다 높은 경우 자동으로 창문을 열 수 있는 시스템을 만들었다. 그렇게 함으로써 20대의 주거환경에 필수적인 공기 오염도를 현저히 줄일 수 있고 전기료 감소로 인한 경제적 이익을 취할 수 있다고 생각한다.

2.3. 제품 상세 특징 및 스펙

66_ict_환기제어 (5)

아두이노 나노 기반인 PCB를 제작하려고 한다. 이 공기청정기는 여러가지 특징이 있다. 브레드 보드를 사용하지 않고 PCB 형태로 제작 되기 때문에 회로가 안정화되어 있고 LED, 미세먼지, 온습도 센서를 통한 정보를 LCD에서 바로 확인을 할 수 있다.
또한 다양한 센서 정보를 다른 쪽 아두이노에 블루투스 통신을 통해서 정보를 보내 내부와 외부의 미세먼지 농도를 비교하여 창문 개폐를 위한 서브모터를 작동시킨다.

2.4. 제품 작동 알고리즘

66_ict_환기제어 (6)

미세먼지에 따른 LED표시, 미세먼지에 농도에 따라 AUTO 펜 제어, 그리고 미세먼지 농도 심각 시 부저가 울리는 것이다. 그리고 그러한 정보를 LCD에 표현하는 것이다.

66_ict_환기제어 (7)

외부장치와 내부장치를 구성하여, HC-06 두개를 페어링하여 외부 미세먼지 농도에 대한 정보를 아두이노에 블루투스 통신을 통해서 보내 창문을 개폐여부를 결정하는 것이다.

66_ict_환기제어 (8)

블루투스 통신의 경우 일련의 String 형식으로 값이 전달이 된다. 따라서 여러가지 정보를 보내기가 매우 어려운 사항이다. 따라서 이번 프로젝트에서는 미세먼지 농도를 포함해서, 온도, 습도, 조도의 정보를 블루투스 통신을 통해서 받아야하기 때문에 센서값별로 일련의 String을 파싱하는 소프트웨어 알고리즘이 필요하다. 그 부분을 코딩으로 구현

66_ict_환기제어 (9)

미세먼지 센서의 값은 정확한 농도를 표현을 하는 것이 아닌 전압을 읽어서 가져오는 것이다. 시리얼 통신을 통해 고정밀 미세저측정기를 통해서, 캘리브레이션 할 수 있는 이동평균 개념을 적용해서 센서값 기복에 대한 부분을 해결했다.

2.5. 회로도 및 부분별 하드웨어 원리

66_ict_환기제어 (10)

캐패시터는 다양한 용도로 사용된다. 노이즈를 막기위해서 맥류신호를 평활하기 위한 평활용으로 사용되었다.

66_ict_환기제어 (11)

전원 순단 시 및 IC의 구동 스피드가 급격히 빨라짐에 따라, 부하전류가 증가한 경우, 전원으로부터의 라인 전압이 강하하여, IC의 오동작을 초래하는 경우가 있다. 이를 방지하기 위해, 전원 라인 정상 시에 콘덴서가 축적해 놓은 전기를 IC 측에 공급하여 전원 라인 전압을 일시적으로 유지한다.

66_ict_환기제어 (12)

아두이노에 전원을 공급하기위해서는 5v전원을 공급해주어야한다. 하지만 모터의 경우 5v를 공급하게 되면 최대 파워를 낼 수 가 없다 그래서 12v전원은 모터에 아두이노에는 전압을 강압시켜 5v를 공급 시켜주기 위해 레귤레이터를 사용하였다. 전원 공급의 경우 12v로 일정한 정전압을 보내므로 스위칭 레귤레이터가 아닌 리니어 레귤레이터를 사용하였다.

66_ict_환기제어 (13)

12v와 0.7A가 사용되는 모턴팬을 동작시키기 위해 Mosfet을 사용하여, 아두이노 신호에 따라 입력된 12V 전원부에서 전류를 공급할 수 있도록 구성을 하였다

66_ict_환기제어 (14)

경고음을 알리기 위한 용도로서 부저를 사용하였다. 부저 또한 코일성분을 가지고 있기에 직접적으로 아두이노에 연결을 하는 것은 좋지 않으며, 아두이노에서 출력하는 전류의 한계로 소리가 매우 작은 측면을 가지고 있다. 따라서 부저의 경우에는 트랜지스터를 사용하여 전류증폭을 통해 더 큰 경고음을 울릴 수 있도록 회로를 구성하였으며, 다이오드(FLYING WEEL 다이오드)를 사용하여, 역기전력를 방지하고자 구성하였다.

3. 최종 결과물

66_ict_환기제어 (1)

66_ict_환기제어 (15)

4.아두이노 소스코드

// 해당 소스코드는 외부데이터(미세먼지 / 온도 / 습도 / 조도) 측정하고, 내부 데이터와 비교하여
// 외부환기에 대한 부분을 담당하는 부분이다.

#include “SoftwareSerial.h”
#include <Adafruit_NeoPixel.h>
#include “DHT.h”
#include <Servo.h>
#include <LiquidCrystal_I2C.h>

// 블루통신 관련해서 필요한 변수 정리
const int maxIndex = 10;
byte blockData[maxIndex]; //block 값 저장
int arrIndex = 0; // 배열 arrIndex

byte refined_humidity = 0;
byte refined_temperature = 0;
byte refined_value = 0;
byte refined_dust_data = 0;

byte refined_humidity2 = 0;
byte refined_temperature2 = 0;
byte refined_value2 = 0;
byte refined_dust_data2 = 0;

/*미세먼지센서 변수들*/
#define measurePin A1 //Connect dust sensor to Arduino A0 pin
#define ledPower 6 //Connect 3 led driver pins of dust sensor to Arduino D4
#define SampleTime 30 //먼지센서 30회 샘플링

int samplingTime = 280;
int deltaTime = 40;
int sleepTime = 9680;
float voMeasured = 0;
float calcVoltage = 0;
float dustDensity = 0;

//조도센서——————————-
#define cds A0

//서보 모터 오브젝트 생성
Servo myservo;

// 온습도 센서를 디지털 4번 핀에 연결합니다.
#define DHTPIN 8
#define DHTTYPE DHT11

//네오픽셀 연결 핀
#define PIN1 4
//네오픽셀 LED개수
#define NUMPIXELS 4
Adafruit_NeoPixel pixels1 = Adafruit_NeoPixel(NUMPIXELS, PIN1, NEO_GRB + NEO_KHZ800);
int delayval = 10;

DHT dht(DHTPIN, DHTTYPE);
SoftwareSerial Serial1(2, 3); // rx,tx

// 서보모터 변수
int val1 = 0;
int val2 = 0;

// LCD의 경우, 고유 주소를 가지고 있는데 대부분의 LCD는 2개의 주소 중 하나로 세팅되어 있다.
// 0x3F, 0×27 이렇게 두가지 주소가 있다.
LiquidCrystal_I2C lcd(0×27, 16, 2);

//—————————————

void GetData();
void get_data_print2();
void data_print1();
void LED_R1();
void LED_B1();
void LED_G1();
void window_open_close(int out, int in);
int dust_val(); // 먼지 값받아오는 함수

void setup () {

// 시리얼통신
Serial.begin(9600);
// 블루투스 통신
Serial1.begin(9600);
//서보 모터 10번핀
myservo.attach(10);
myservo.write(100);
// 조도센서
pinMode(cds, INPUT);
// 미세먼지센서
pinMode(ledPower, OUTPUT);
pinMode(measurePin, INPUT);
//온도습도센서 시작
dht.begin();
// 네오픽셀 시작
pixels1.begin(); // This initializes the NeoPixel library.

lcd.init();
lcd.backlight();
lcd.clear();
}

void loop () {

// 블루투스 통신을 통해 내부데이터를 받아서 파싱을 하고, 각 변수에 저장

if (Serial1.available()) {
GetData();
}
if (arrIndex != 0 && arrIndex < 9) {
Serial.println(“내부데이터 변수 저장”);

if (blockData[0] == 104)
refined_humidity2 = blockData[1];
if (blockData[2] == 116)
refined_temperature2 = blockData[3];
if (blockData[4] == 99)
refined_value2 = blockData[5];
if (blockData[6] == 100)
refined_dust_data2 = blockData[7];
} else {

Serial.println(“내부데이터 변수 이상으로 미저장 or 블루투스 미연결—–”);
}

// 습도와 온도값을 측정하고, 제대로 측정되었는지 확인해줍니다.
byte humidity = dht.readHumidity();
byte temperature = dht.readTemperature();

refined_humidity = constrain(humidity, 0, 100);
refined_temperature = constrain(temperature, 0, 100);

// 조도센서 값
int value = analogRead(cds);
refined_value = map(value, 0, 1023, 100, 0);

// 먼지센서 값
int dust_data = dust_val(); // 먼지량을 받아옴
refined_dust_data = map(dust_data, 0, 1023, 0, 100);

// 미세먼지 농도에 따라 색변화와 부저를 활용하여 경고음 제공하기.

if (refined_dust_data > 65) {
delay(10);
LED_R1();
} else if (refined_dust_data <= 65 && refined_dust_data > 50 ) {
delay(10);
LED_RB1();
} else if (refined_dust_data <= 50 && refined_dust_data > 25) {
delay(10);
LED_B1();
} else {
LED_G1();
}

// 내부데이터와 외부데이터를 비교하여, 창문개폐 진행
// 내부 공기청정기팬의 경우에는 외부,내부 미세먼지 농도에 상관없이, 내부 미세먼지가 25 이상인 경우에는 자동으로 작동이된다.
// 창문개폐의 경우는
// 1) 외부 창문이 열리는 경우
// 외부의 미세먼지농도가 내부보다 낮을 때,
// 외부 미세먼지 농도가 높은 경우에는 창문을 열리지 않고, 내부 팬에 의해서 공기정화가 진행이 된다.
// 내부 미세먼지 농도가 25 이상일 때, 동작한다.
window_open_close(refined_dust_data, refined_dust_data2);

lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“out dust :”);
lcd.print(refined_dust_data);
lcd.setCursor(0, 1);
if(refined_dust_data2 ==0){
lcd.print(“BT disconnected”);
}else{
lcd.print(“inner dust :”);
lcd.print(refined_dust_data2);
}

get_data_print2();
data_print1();
}

//—————————————
void LED_R1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(255, 0, 0)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_G1() {

for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(0, 255, 0)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_B1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(0, 0, 255)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_RB1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(255, 0, 120)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

//—————————————
int dust_val() //센서의 노이즈 값으로인해 여러번 데이터를 받은뒤 평균 먼지량을 구함
{
int dust = 0;
for (int i = 0; i < SampleTime; i++)
{
digitalWrite(ledPower, LOW); // power on the LED 0.028초부터 led가 가장 밝음으로 최소샘플링 타임으로 결정
delayMicroseconds(samplingTime);

voMeasured = analogRead(measurePin); // read the dust value
delayMicroseconds(deltaTime);
digitalWrite(ledPower, HIGH); // turn the LED off
delayMicroseconds(sleepTime);
//////////////////////////////////////////// dust vlaue 받아오기
calcVoltage = voMeasured * (5.0 / 1024.0);
dustDensity = calcVoltage / 0.005; //(0.17 * calcVoltage – 0.1) * 1000;
/////////////////////value 의 미세한 변화를 수식으로 먼지량으로 변환
if (dustDensity < 0) { // value 의 값이 마이너스일경우 오류 임으로 0을 대입
dustDensity = 0;
}
dust += dustDensity;
}
return (dust / SampleTime)+50; // 센싱한값의 평균을 return
}

void GetData() {
Serial.println();
Serial.println(“데이터 가져오기 시작 “);
char temp;
arrIndex = 0;

temp = (char)Serial1.read();
Serial.println(“”);
Serial.print(temp);

// 전송 시작 확인
// 블루투스 시작을 알리는 문자를 s로 설정 !!!
if (temp != ‘s’) {
while (temp != ‘e’ ) {
temp = (char)Serial1.read();
}
return;
}
String stringTemp;

while (true) {
// Serial.print(“arrIndex : “);
// Serial.println(arrIndex);

if (Serial1.available()) {
temp = (char)Serial1.read();
Serial.print(temp);

if (arrIndex > 10) {
// Serial.println(“내부데이터 변수 이상 ———-”);
break;
}

// ‘_’ 라는 문자를 통해 구분을 지어준다. 구분을 지어주는 표시라고 생각을 하면된다. ———–
if (temp != ‘_’) {

// ‘e’ 라는 문자가 나오게 되면 블루투스 통신이 끝났다는 의미이다.
if (temp == ‘e’) {
break;
}

if (isAlpha(temp)) { //temp라는 변수가 문자인지 확인을 한다.

blockData[arrIndex] = temp;
arrIndex++;

} else if (isDigit(temp)) { //temp라는 변수가 숫자인지 확인을 한다.
stringTemp += temp;

} else {
// 원하지 않는 데이터 값
Serial.println(“upload failed. error: not digit or alpha”);
arrIndex = 0;
return;
}
}

else if (temp == ‘_’) {
if (stringTemp == “”)
continue; // 아래의 것들을 건너뛴다.
blockData[arrIndex] = stringTemp.toInt();
//변수 초기화
stringTemp = “”;
arrIndex++;
}
}
}
Serial.println(“끝”);
Serial.println(“”);
delay(30);
}

// int out은 외부미세먼지농도, int in 내부미세먼지농도
void window_open_close(int out, int in) {

// 내부미세먼지농도가 낮아서 공기정화를 할 필요없는 경우
if(in < 3){
return;}
if (in < 25) {
Serial.println(“내부 공기가 매우 깨끗합니다.”);
val1 = 100;
if (val1 != val2) {
myservo.write(val1); // 창문이 닫힘.
val2 = val1;
delay(10);
}
return;
}
// 내부미세먼지농도가 안좋은 경우
else {
if (out < in) {
Serial.println(“환기를 위해 창문 open. “);
val1 = 0;
if (val1 != val2) {
myservo.write(val1); // 창문이 닫힘.
val2 = val1;
delay(10);
}
delay(10);
} else {
Serial.println(“외부 미세먼지 농도가 높아, 창문 close. “);
val1 = 100;
if (val1 != val2) {
myservo.write(val1); // 창문이 닫힘.
val2 = val1;
delay(10);
}
delay(10);
}
}
}

void data_print1() {
Serial.print(“외부센서데이터 : “);
Serial.print(“humidity : “);
Serial.print(refined_humidity);
Serial.print(” temp :”);
Serial.print(refined_temperature);
Serial.print(” cds : “);
Serial.print(refined_value);
Serial.print(” dust : “);
Serial.println(refined_dust_data);
delay(300);
}

void get_data_print2() {
Serial.print(“내부센서데이터 : “);
Serial.print(“humidity : “);
Serial.print(refined_humidity2);
Serial.print(” temp :”);
Serial.print(refined_temperature2);
Serial.print(” cds : “);
Serial.print(refined_value2);
Serial.print(” dust : “);
Serial.println(refined_dust_data2);
delay(300);
아두이노 코드 2 – 내부 담당 코드
#include “SoftwareSerial.h”
#include <Adafruit_NeoPixel.h>
#include “DHT.h”
// I2C LCD를 쉽게 제어하기 위한 라이브러리를 추가해줍니다.
#include <LiquidCrystal_I2C.h>

/*미세먼지센서 변수들*/
#define measurePin A1 //Connect dust sensor to Arduino A0 pin
#define ledPower 6 //Connect 3 led driver pins of dust sensor to Arduino D4
#define SampleTime 30 //먼지센서 30회 샘플링

int samplingTime = 280;
int deltaTime = 40;
int sleepTime = 9680;
float voMeasured = 0;
float calcVoltage = 0;
float dustDensity = 0;
// 정제된 변수값
byte refined_humidity = 0;
byte refined_temperature = 0;
byte refined_value = 0;
byte refined_dust_data = 0;

//조도센서——————————–
#define cds A0

//부저 ———————————
#define buzzer 7

//모터팬 ——————————
#define fan 9

// 온습도 센서를 디지털 4번 핀에 연결합니다.
#define DHTPIN 8
#define DHTTYPE DHT11

//——————————-

//네오픽셀 연결 핀
#define PIN1 4

//네오픽셀 LED개수
#define NUMPIXELS 4
Adafruit_NeoPixel pixels1 = Adafruit_NeoPixel(NUMPIXELS, PIN1, NEO_GRB + NEO_KHZ800);
int delayval = 10;
int color_value=0;

DHT dht(DHTPIN, DHTTYPE);
SoftwareSerial Serial1(2, 3); // rx,tx

// LCD의 경우, 고유 주소를 가지고 있는데 대부분의 LCD는 2개의 주소 중 하나로 세팅되어 있다.
// 0x3F, 0×27 이렇게 두가지 주소가 있다.
LiquidCrystal_I2C lcd(0×27, 16, 2);

//—————————————-
void SendData();
void LED_RB1();
void setup () {

// 시리얼통신
Serial.begin(9600);
// 블루투스 통신
Serial1.begin(9600);
// 조도센서
pinMode(cds, INPUT);
// 미세먼지센서
pinMode(ledPower, OUTPUT);
pinMode(measurePin, INPUT);
//온도습도센서 시작
dht.begin();
// 12V 모터팬
pinMode(fan, OUTPUT);
// 네오픽셀 시작
pixels1.begin(); // This initializes the NeoPixel library.

// 부저 // #define buzzer 7
pinMode(buzzer, OUTPUT);
lcd.init();
lcd.backlight();
lcd.clear();
}
int dust_val(); // 먼지 값받아오는 함수
void loop () {

// 습도와 온도값을 측정하고, 제대로 측정되었는지 확인해줍니다.
byte humidity = dht.readHumidity();
byte temperature = dht.readTemperature();

refined_humidity = constrain(humidity, 0, 100);
refined_temperature = constrain(temperature, 0, 100);

// 조도센서 값
int value = analogRead(cds);
refined_value = map(value, 0, 1023, 100, 0);
color_value=map(refined_value, 10, 100, 0, 255);

// 먼지센서 값
int dust_data = dust_val(); // 먼지량을 받아옴
refined_dust_data = map(dust_data, 0, 1023, 0, 100);

Serial.print(” temp : “);
Serial.print(refined_temperature);
Serial.print(“‘c “);
Serial.print(” humidity : “);
Serial.print(refined_humidity);
Serial.print(“% “);
Serial.print(” cds : “);
Serial.print(refined_value);
Serial.print(” dust : “);
Serial.println(refined_dust_data);

lcd.clear();
lcd.setCursor(0, 0);
lcd.print(“hm:”);
lcd.print(refined_humidity);
lcd.print(” tp:”);
lcd.print(refined_temperature);
lcd.setCursor(0, 1);
lcd.print(“cds :”);
lcd.print(refined_value);
lcd.print(” dust :”);
lcd.print(refined_dust_data);

// 미세먼지 농도에 따라 색변화와 부저를 활용하여 경고음 제공하기.

if (refined_dust_data > 68) {
analogWrite(fan,150);
delay(10);
LED_R1();
tone(buzzer, 1000);
} else if (refined_dust_data <= 68 && refined_dust_data > 50 ) {
analogWrite(fan,150);
noTone(buzzer);
delay(10);
LED_RB1();
} else if (refined_dust_data <= 50 && refined_dust_data > 25) {
analogWrite(fan, 120);
noTone(buzzer);
delay(10);
LED_B1();
} else {
analogWrite(fan, 0);
noTone(buzzer);
LED_G1();
}

SendData();
}

//——————-

void LED_R1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(color_value, 0, 0)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_RB1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(color_value, 0, color_value/2)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_G1() {

for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(0, color_value, 0)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void LED_B1() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels1.setPixelColor(i, pixels1.Color(0, 0, color_value)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}

void rain_color1() {

for (int i = 0; i < NUMPIXELS; i++) {
int a = random(0, 255);
int b = random(0, 255);
int c = random(0, 255);

pixels1.setPixelColor(i, pixels1.Color(a, b, c)); // Moderately bright green color.
pixels1.show(); // This sends the updated pixel color to the hardware.
delay(delayval); // Delay for a period of time (in milliseconds).
}
}
//int dust_val() //센서의 노이즈 값으로인해 여러번 데이터를 받은뒤 평균 먼지량을 구함
{
int dust = 0;
for (int i = 0; i < SampleTime; i++)
{
digitalWrite(ledPower, LOW); // power on the LED 0.028초부터 led가 가장 밝음으로 최소샘플링 타임으로 결정
delayMicroseconds(samplingTime);

voMeasured = analogRead(measurePin); // read the dust value
delayMicroseconds(deltaTime);
digitalWrite(ledPower, HIGH); // turn the LED off
delayMicroseconds(sleepTime);
////////////////////////////////// dust vlaue 받아오기
calcVoltage = voMeasured * (5.0 / 1024.0);
dustDensity = calcVoltage / 0.005; //(0.17 * calcVoltage – 0.1) * 1000;
////////////value 의 미세한 변화를 수식으로 먼지량으로 변환
if (dustDensity < 0) { // value 의 값이 마이너스일경우 오류 임으로 0을 대입
dustDensity = 0;
}
dust += dustDensity;
}
return dust / SampleTime; // 센싱한값의 평균을 return
}

void SendData() {
Serial1.print(“s_”);
Serial1.print(“h_”);
Serial1.print(refined_humidity);
delay(3);
Serial1.print(“_t_”);
Serial1.print(refined_temperature);
delay(3);
Serial1.print(“_c_”);
Serial1.print(refined_value);
delay(3);
Serial1.print(“_d_”);
delay(3);
Serial1.print(“_e”);
delay(200);
}

 

 

[66호] Automatic Serving & Order System

66 ict_오더시스템 (2)

66_ict_환기제어 (1)

2020 ICT 융합 프로젝트 공모전 장려상

Automatic Serving & Order System

 

글 | 서울시립대학교 박진석, 서재원, 송태헌

1. 심사평
칩센 라인트레이서의 기본 기능에 서버 시스템과의 연계를 통한 특정 위치를 찾아가게 하는 동작으로 무인 서버 로봇을 구성한 것으로, 사실 특이할 만한 점은 보이지 않아 보입니다. 모터구동과 관련하여 부품을 선정하고, 적용하기 위하여 사전에 촘촘한 선행 검토와 모델링을 진행한 것은 매우 잘 진행 된 듯 합니다. 최근에 Indoor-position 관련한 솔루션이 매우 활발하게 검토되고 개발이 되고 있는데, 이를 이용하여 라인트레이싱이 아닌 일반적인 환경에서의 서버 로봇을 만들 수 있다면 최근 트렌드와 맞는 더 진보한 작품이 되지 않을까 하는 생각이 듭니다.

펌테크 아이디어와 실용성, 상업성을 두루 갖춘 작품이라고 생각합니다. 전체적으로 세심하고 짜임새 있게 잘 구성이 되었다고 생각되며 기술적 구현도, 작품 완성도 등이 높은 작품이라고 생각합니다.

위드로봇 이동로봇의 주행 중에 발생하는 문제는 어떻게 해결하였는지 고민이 더 추가되면 좋은 작품이 될 것 같습니다.

2. 개요
본 프로젝트는 식당에서 사용할 수 있는 자동으로 서빙을 해주는 무인 서빙 로봇, 주문을 해주는 어플리케이션과 주문을 확인하고 서빙 명령이 가능한 웹페이지 제작을 한다. 서빙 로봇은 Wi-Fi와 연결되었기 때문에 공유기가 설치된 환경이면 어디서든 환경이 가능하고, 주문한 음식은 서버에 저장되기 때문에 완벽한 IoT를 구현하였다.

2.1. 개발 배경
현대사회에서 생산성과 효율을 위해서 많은 분야에서 무인/자동화가 이루어지고 있다. 이러한 추세에 따라 매장에 무인 주문 결제기인 ‘키오스크’가 설치되고 있고, 일부 매장은 어플로 주문을 하는 시스템을 도입했다. 따라서 위의 주문 시스템에 자동으로 서빙을 로봇을 융합함으로 IoT를 이용한 무인 서비스 제공을 목표로 한다.

2.2. 프로젝트 목적 및 기대효과
이 프로젝트의 목적은 주문/서빙 시스템의 자동화이다. 병렬적으로 주문을 할 수 있는 무인 주문 서비스와 무인 서빙 서비스를 만드는 것이 목표이다. 무인 서빙 서비스가 있기 때문에 서빙에 필요한 인건비가 줄어들어 경제적이고 효율적이다. 무인 주문 서비스를 통해서 순간적으로 사람이 집중이 되더라도 주문하는데 기다릴 필요가 없게 된다. 또한, 주방에서 출력된 종이 영수증을 보통 주문의 순서를 정하는데, 이 프로젝트를 통해서 종이 영수증대신 태블릿 PC와 같은 전자기기로 대체할 수 있을 것이다. 이에 따라 주방에서 낭비되는 종이의 양을 줄여서 환경보호면에서도 효과가 있다. 또한, 보드는 Arduino UNO만을 사용하고 성능을 대부분 사용하여 개발을 했기 때문에 실제 제품으로 만들 시 제품 비용 절감이 매우 절감될 것이다.

2.3. 개발 목표
서빙 할 테이블의 번호를 지정하면 자동으로 서빙을 해주는 서비스 구현과 많은 사람들이 집중되더라도 한꺼번에 처리가 가능한 무인 병렬 주문 서비스 제공이 목표이다.

2.4. 세부 개발 내용
네이버 클라우드에 비용을 지불해서 구축한 서버에 APM(Apache2, PHP, MySQL)을 설치하고, 웹 페이지와 안드로이드 어플을 제작하여 주문한 데이터가 DB인 MySQL에 저장되도록 한다. 관리자는 관리자용 웹페이지를 통해서 MySQL에 저장된 데이터를 확인해서 들어온 주문을 확인할 수 있다. 서빙을 원하는 테이블을 웹 페이지에 입력하면 마찬가지로 MySQL에 저장이 된다. 서빙 로봇은 매장에 설치된 Wi-Fi와 연결된 상태로 구동되고, 일정 간격으로 MySQL을 확인해서 서빙 요청이 왔는지 확인한다. 서빙 명령을 받게 되면 목표 테이블까지 라인트레이싱을 하며 도달하게 된다. 이 때 서빙 하는 음식의 무게에 상관없고, 가속도에 의한 음식의 미끄러짐을 최소화하기 위해서 모터 PID속도 제어를 통해서 운동한다.

3. 프로젝트 설명
3.1. 주요 동작 및 특징

66 ict_오더시스템 (1)

서빙로봇이 Wi-Fi에 연결된 상태로 1초간격으로 MySQL 데이터를 읽어들이며 주문을 기다리고 있다.

66 ict_오더시스템 (2)

원하는 음식과 테이블을 선택해서 MySQL로 데이터를 전달해주는 어플리케이션이다.

66 ict_오더시스템 (3)

MySQL에 저장된 데이터를 읽을 수 있고, 테이블번호를 선택하면 Arduino Called열 값을 1로 바꿔서 서빙 로봇에게 서빙 명령을 하는 관리자용 웹 페이지이다. (http://106.10.49.227/order_list.php)

66 ict_오더시스템 (4)

서빙 로봇이 Wi-Fi를 통해서 MySQL에서 Serving Completed열이 0이고 Arduino Called가 1인 테이블 번호를 읽어 들어서 해당 테이블로 음식을 서빙하고 있다.

66 ict_오더시스템 (2)

서빙이 완료된 후 MySQL의 Serving Completed열 값을 1로 변경해서 서빙이 완료됨을 알리는 서빙 로봇 창이다.

3.2. 전체 시스템 구성
3.2.1. Software Architecture_System Flowchart

66 ict_오더시스템 (3)

66 ict_오더시스템 (4)

손님이 주문을 하고 서빙 로봇(아두이노)이 서빙을 하기 위한 방법으로 서버를 사용하였다. 손님이 주문을 하면 주문내역이 서버의 데이터베이스에 저장이 되고 주방에서는 서버의 데이터베이스를 확인하여 서빙 로봇을 호출한다. 서빙 로봇에서는 와이파이에 연결되어 있으며 일정한 간격으로 서버의 데이터베이스에 접근하여 호출 신호가 왔을 때, 지정된 테이블로 음식을 서빙 한다.

3.2.2. Hardware Architecture

66 ict_오더시스템 (5)

3.3. 개발 환경(개발 언어, Tool, 사용 시스템 등)

66 ict_오더시스템 (6)

4. 단계별 제작 과정
4.1. 서버구축

66 ict_오더시스템 (7)

서버는 네이버 클라우스 서비스를 사용하여 구축하였다. 기본적으로 유료서비스이지만 작은 규모의 서버를 1년동안 무료로 사용할 수 있는 이벤트가 있어서 네이버 서버를 선택하였다. 서버의 사양은 [Micro] 1vCPU, 1GB Mem, 50GB Disk이라 성능은 좋지 않지만 이번 프로젝트에서는 문제가 없다고 판단하였다. 서버의 OS는 Ubuntu 16.04 LTS Server를 설치하였다.

4.2. APM(Apache2/PHP/MySQL) 설치 및 ACG 설정

66 ict_오더시스템 (8)

66 ict_오더시스템 (9)

네이버클라우드에서 할당받은 서버에 APM을 설치한 뒤 공인 IP인 http://106.10.49.227/에 접근할 수 있도록 80번 포트를 ACG를 통해서 열어주었다. 인터넷이 연결된 어떤 환경에서도 해당 주소로 이동하면 Apache2디폴트 페이지를 확인할 수 있다.

4.3. MySQL 테이블 구축

66 ict_오더시스템 (10)

Windows에서 MySQL Workbench를 설치하여 좀 더 쉽게 MySQL를 다룰 수 있도록 하였다. orderDB의 테이블 속, 각 데이터들은 다음을 의미한다. _order는 주문순서, time는 주문시간, _table는 주문 테이블 번호, food는 주문한 음식과 수량, serving는 서빙이 완료되었는지 안되었는지, call_arduino는 서빙 로봇이 확인하는 데이터로써 값이 1일 경우, 서빙 로봇이 음식을 서빙한다.
_order열에는 값을 입력하지 않아도 자동으로 값이 1씩 증가하도록 설정하였고, time은 ubuntu서버에 있는 시간을 자동으로 입력받도록 설정하였다. 아래에 기술할 웹페이지에서 _table, food, call_arduino열을 설정할 수 있도록 연동하였다.

4.4. 웹 페이지 및 애플리케이션 제작
4.4.1. 주문 애플리케이션 제작
AndroidStudio를 이용하여 주문 애플리케이션을 개발하였다. 메뉴 리스트 화면, 메뉴 상세 화면, 장바구니 화면으로 구성되었다.

66 ict_오더시스템 (11)

메뉴 리스트 화면: 처음 앱을 켰을 때, 나타나는 화면이다. 내부 데이터베이스에서 저장된 모든 메뉴의 사진 썸네일과 이름, 가격을 표시하며, 각 메뉴를 선택하면 메뉴 상세화면으로 넘어간다.
메뉴 상세화면: 메뉴 상세화면은 메뉴 리스트 화면에서 선택된 메뉴의 사진과 이름, 가격을 확인할 수 있으며, 수량을 체크하고 ‘장바구니 담기’버튼을 선택하여, 내부 데이터베이스에 저장한다. 만약 해당 메뉴가 이미 데이터베이스에 저장되어 있으면 해당 메뉴의 수량만 변경한다.
장바구니 화면: 장바구니 화면은 모든 화면에서 액션바에 있는 버튼을 통해 접근할 수 있다. 데이터베이스에 저장된 장바구니 리스트를 가져와서 보여준다. x버튼을 누르면, 해당 메뉴를 데이터베이스에서 지움으로 장바구니 리스트에서 지운다. 결제하기 버튼을 통해 서버와 Http통신을 하며, 테이블 번호와 함께 장바구니 리스트를 전달한다. 서버로부터 RESULT_OK 신호가 오면, “결제가 완료되었습니다.”라는 안내와 함께 장바구니 리스트를 모두 지운다.

4.4.2. 관리자용 웹 페이지 제작

66 ict_오더시스템 (12)

66 ict_오더시스템 (13)

이미 구축한 리눅스 서버에 Putty를 사용해서 터미널 접속을 한 후 /var/www/html/ 경로에 php파일을 생성하면 위와 같이 공인아이피 http://106.10.49.227/파일이름.php 로 접속할 수 있다. 본 프로젝트에서 /var/www/html /order_list.php에 php와 html을 사용해서 코딩했다.
손님용 웹페이지에서 입력한 MySQL 정보를 읽어와서 표로 표현해주는 기능을 한다. 요리가 완료되면 아래에 있는 테이블 번호를 입력하고 ‘제출’버튼을 누르면 Arduino Called열이 1로 바뀌어 서빙 로봇에게 서빙 명령을 내릴 수 있다.

4.5. Arduino에서 Wi-Fi통신을 통한 MySQL 데이터 수신
Arduino에서 Wi-Fi통신을 한 후에 MySQL로 바로 접근해서 query를 입력해주면 MySQL에서 해당하는 기능을 실행하는 동작을 구현하였다. 소스코드에서 char query[] = “SELECT _table FROM project.orderDB WHERE call_arduino = ’1′ AND serving = ’0′ ;”; char query2[] = “UPDATE project.orderDB SET serving = ’1′ WHERE call_arduino = ’1′ AND serving = ’0′ ;”; 에 명령을 저장하고, cur_mem->execute(query); 명령어로 query배열에 담긴 내용을 MySQL에 입력할 수 있다. 여기에서 query는 MySQL에서 call_arduino 열 값이 1이고 serving 열 값이 0인 _table열의 값을 return하는 기능을 하고, query2는 call_arduino열 값이 1이고 serving 열 값이 0인, 즉 query에서 return한 _table열의 같은 값의 serving열을 1로 바꿔주는 기능을 담당한다.

66 ict_오더시스템 (14)

66 ict_오더시스템 (15)

 

위의 그림은 serving열의 값이 0이고 call_arduino열의 값이 1인 (서빙 명령을 받은 행) 테이블 번호의 MySQL값을 1초마다 Arduino에서 읽어 들이는 모습을 나타낸다.

4.6. DC모터의 PID Feedback Control

66 ict_오더시스템 (16)

위의 모터의 Specification을 참고해서 [T:토크(kgf-cm) V:전압(V), I:전류(A), w:각속도(rad/s), R:저항(Ω)] 아래의 관계식을 세울 수 있다.

T∝
V=IR

두 개의 식을 연립하고 비례상수 k를 도입하면 아래의 식으로 변환이 가능하다.

Tw=k

Stall Torque는 최대 토크의 값을 의미하고, 이는 최대 전압인 6V와 Stall Current인 3.2A일 때 성립한다. 따라서 옴의 법칙에 의해서

6=3.2R
R=1.875Ω

모터의 저항은 1.875Ω이다.
또한, DC모터의 스펙으로부터 전류에 다른 토크의 값을 아래와 같이 기재할 수 있다.
DC모터, 아두이노 차체 시스템에서 차체를 모델링하여 전달함수를 구하기 위해 Free Body Diagram을 그려서 계산할 필요가 있다.

66 ict_오더시스템 (17)

66 ict_오더시스템 (18)

위의 그림에서 오른쪽에 있는 두 개의 원이 모터가 달린 바퀴이고, 왼쪽에 있는 원은 캐스터이다. 바닥과의 마찰이 충분하여 바퀴가 헛도는 상황이 발생하지 않는다고 가정하면, 모터에 의한 토크는 바퀴 두 개에 가해지는 힘으로 생각할 수 있다. 위의 그림을 다시 차체에 대해서만 Free Body Diagram을 그리면 아래와 같이 간단한 형태로 나타낼 수 있다.

66 ict_오더시스템 (19)

모터에 의한 추진력 2F와 공기저항과 같은 저항에 의한 반력이 생기기 때문에 cv를 도입하였으나, 영향은 작을 것으로 예측되어 c=0.1인 낮은 값으로 가정하였다. 바퀴의 반지름이 3.25cm인 것을 고려하여 운동방정식을 세우면 아래와 같다.

66 ict_오더시스템 (20)

여기에서 은 비선형이므로, 라플라스 변환을 하기 위하여 테일러 급수를 이용해 정상상태에서의 선형화한 값을 도출해낸다.

66 ict_오더시스템 (21)

차체의 무게는 약 400g이고, 정상상태에서 가속도는 작은 것을 목표로 =0.01cm/s2을 대입하여 이 식을 정리한다.

66 ict_오더시스템 (22)

Fitting Line에서 구한 T=2.844·I+1.088을 대입하여 위의 식을 정리한다.

66 ict_오더시스템 (23)

상수를 모두 없애기 위해서, 이 식을 편차변수로 나타낼 수 있다.

66 ict_오더시스템 (24)

옴의 법칙을 이용해서 전류를 전압에 대한 식으로 바꾼다.

66 ict_오더시스템 (25)
위 식을 라플라스 변환하면 다음과 같이 표현할 수 있다.

66 ict_오더시스템 (26)

이 전달함수를 구할 수 있다.

66 ict_오더시스템 (27)
이 식을 바탕으로 MATLAB을 활용하여 Simulink를 구성하면 다음과 같다.

66 ict_오더시스템 (28)

 

66 ict_오더시스템 (29)

 

MATLAB에 내장된 PID Tuner를 통해서 PID 계수를 각각 4.227, 2.297, -0.03976으로 결정했다.

 

66 ict_오더시스템 (30) 66 ict_오더시스템 (31)

예상된 시뮬레이션으로, 외란(서빙 로봇의 무게 변화)이 존재하는 경우와 존재하지 않는 경우에도 모두 목표 값을 잘 찾아가는 것을 확인할 수 있다.

66 ict_오더시스템 (32)

위의 그림은 아두이노에서 DC모터를 실제로 구동하고 시리얼 모니터에서 받아온 Encoder값을 Simulink에서 예측한 그래프에 도사한 것이다. 필터를 사용하지 않았기 때문에 Encoder 자체에서 발생한 노이즈에 의해서 벗어나는 데이터가 보이지만 평균적으로 목표 속도인 30cm/s에 수렴하기 때문에 안정적으로 제어에 성공하였다.

4.7. 아두이노 모터와 센서 핀 구성

66 ict_오더시스템 (33)

IR센서는 아날로그 신호가 필요하기 때문에 아날로그 핀인 A1, A2, A3, A4를 사용하였다. 모터의 경우는 모터의 속도를 조절하기 위해 PWM 신호를 줄 수 있는 5,6 10,11번 핀을 사용하였다.
모터의 엔코더의 경우는 인터럽트 신호를 사용한다. 따라서 아두이노 우노에서 인터럽트 신호를 사용할 수 있는 유일한 핀인 2번 3번 핀을 사용하였다. 모터의 2개 모두 엔코더를 사용하고 싶었으나 아두이노 우노에는 핀이 부족하여 모터 하나의 엔코더만 사용하기로 하였다.
Wi-Fi를 연결을 위해 ESP-01 어뎁터는 8, 9번 핀을 사용하였다.

4.8. 원하는 테이블로 찾아가는 알고리즘 제작
기본적으로 길찾기와 라인트레이서를 분리를 하여 제작을 하였다. 전체적으로 원하는 테이블로 가기 위해서 길을 찾아가는 알고리즘이 돌아가고 길 찾기 알고리즘에서는 모터에게 세부적인 명령을 내리지 않는다. 앞으로 갈지, 좌회전이나 우회전을 할 지 서빙 로봇에게 명령만 내리도록 하였다. 그리고 서빙 로봇은 명령을 받는 대로 라인트레이서 모듈을 활용하여 모터를 가동하도록 하였다.
원하는 테이블로 가기 위해서 교차로를 탐지하였다. 각 테이블마다의 고유의 코스별로 명령들을 저장을 하였다. 예를들어 1번 테이블의 경우는 [직진, 좌회전(첫번째 교차로), 직진]이고 4번 테이블의 경우는 [직진, 우회전(두번째 교차로, 직진)]이다. 교차로를 탐지하는 방법으로는 IR sensor의 양끝부분에서 모두 검정색이 감지가 되면 교차로라고 판단을 하였다. 양끝부분 사이의 거리가 도로의 폭보다 넓기 때문에 가능하다.

4.9. 상황 별 IR sensor 값에 따른 라인트레이서

66 ict_오더시스템 (35) 66 ict_오더시스템 (34)

직진을 하라는 명령이 들어왔을 경우, IR sensor 값 중에서 가운데 두개가 모두 검정색일 때에는 도로 위에 잘 있는 경우이므로 양 모터의 속도를 일정하게 주었다. 하지만 가운데 두개중에서 하나라도 검정색이 아닌 경우에는 도로를 이탈하려는 경우이므로 양 모터의 속도를 다르게 주어서 원래 도로로 복귀할 수 있도록 하였다.
좌회전, 우회전 하라는 명령이 들어왔을 경우에는 양 끝 센서의 값을 확인하면서 회전을 하도록 하였다. 예를들어 좌회전을 하는 경우 가장자리 우측 센서의 값이 검, 흰, 검, 흰 순서대로 바뀌게 된다. 따라서 우측 센서의 값이 바뀔 때마다 카운트를 하도록 하였다. 카운트가 3번되는 순간 회전이 끝났다고 판단을 하였다.

4.10. 서빙 로봇 프레임 및 모형 식당 제작

66 ict_오더시스템 (5) 66 ict_오더시스템 (6)

아두이노 RC카용 프레임에 IR센서, Arduino UNO, Wi-Fi 모듈, 바퀴 등을 조립했다. 배터리와 DC 모터는 프레임의 하단부에 조립했고, 음식을 싣기 위해서 프레임과 조립할 수 있는 받침대를 CATIA로 모델링해서 3D 프린터로 출력하였다.

5. 소스코드
프로젝트 깃허브 주소 : https://github.com/ParkJinSuk/ASOS-AutomaticServingOrderSystem

FINDTABLE

void lineTracing()
{
/* 0.5초마다 라인트레이서 모듈 센서 측정 */
if((millis() – time) >= SENSING_PERIOD)
{
time = millis();
getIRSensor();
// showIRSensor();
if(isCrosswalk()){
Serial.println(F(“갈림길 감지”));
crosswalkCnt += 1;
if(isTurningTime(crosswalkCnt)){//회전할 갈림길
if(turn[target_table] == LEFT)
Serial.println(F(“왼쪽”));
else if(turn[target_table] == RIGHT){
Serial.println(F(“오른쪽”));
}
moveTracer(turn[target_table]);//회전한다.
}else{
Serial.println(F(“갈림길 통과”));
moveTracer(FORWARD_THROUGH_CROSSWALK);//갈림길 통과
}

}else if(isRoad()){
Serial.println(F(“straight”));
moveTracer(STRAIGHT);//앞으로 이동.
}else{
Serial.println(F(“stop”));
moveTracer(STOP);//멈춘다.
isDone = true;
}
}

}

bool isCrosswalk(){
return !RightOut && !LeftOut;//검검이면 갈림길!!
}

bool isTurningTime(int cnt){
Serial.println(cnt);
return cnt == crosswalk[target_table];
}

bool isRoad(){
if (!LeftOut || !LeftIn || !RightIn || !RightOut)
{
cnt_isRoad = 0;
return true;
}
else
{
if (cnt_isRoad != 5){cnt_isRoad += 1; return true;}
if (cnt_isRoad == 5){cnt_isRoad = 0; return false;}
}
}

void moveTracer(int dir){
// Serial.print(“moveTracer”);
// Serial.println(dir);

switch(dir){
case STRAIGHT : moveForward();break;
case LEFT : turnLeft();break;
case RIGHT : turnRight();break;
case STOP : stopMoving();break;
case FORWARD_THROUGH_CROSSWALK : moveThroughCrosswalk();break;
}
}
/* 라인트레이서 모듈을 사용하여 검은색 선을 따라가는 함수 */
void moveForward()
{
if (!LeftIn && !RightIn)
{
// Serial.println(“forward 11″);
pidControl_Hz(Hz, 20);
}
else if (LeftIn && !RightIn)
{
// Serial.println(“forward 01″);
MotorA(STRAIGHT, STRAIGHT_SPEED_WEEK);
MotorB(STRAIGHT, STRAIGHT_SPEED_STRONG);
}
else if (!LeftIn && RightIn)
{
// Serial.println(“forward 10″);
MotorA(STRAIGHT, STRAIGHT_SPEED_STRONG);
MotorB(STRAIGHT, STRAIGHT_SPEED_WEEK);
}
else
{
// Serial.println(“forward xx”);
pidControl_Hz(Hz, 20);
}

}

void turnLeft()
{
// Serial.println(“turn left”);

while(!RightOut || !LeftOut){//하양이면 끝. 도는 동안은 계속 검정.
if((millis() – time) >= SENSING_PERIOD){
time = millis();
getIRSensor();
}
Serial.println(F(“왼쪽~~~~~~~~~~~~~~~~”));
MotorA(STRAIGHT, TURN_SPEED);
MotorB(STRAIGHT, 0);
}
Serial.println(F(“왼쪽~~~~~~~~~~~~끝~~~~”));

}
void turnRight()
{
// Serial.println(“turn right”);

while(!RightOut || !LeftOut){//하양이면 끝. 도는 동안은 계속 검정.
if((millis() – time) >= SENSING_PERIOD){
time = millis();
getIRSensor();
}
// if(LeftOut != pre){
// chgCnt += 1;
// pre = LeftOut;
// }
Serial.println(F(“오른쪽~”));

MotorA(STRAIGHT, 0);
MotorB(STRAIGHT, TURN_SPEED);
}
Serial.println(F(“오른끝쪽”));
}
void stopMoving()
{
// Serial.println(“stop”);
MotorA(STOP, 0);
MotorB(STOP, 0);
}

void moveThroughCrosswalk(){
// Serial.println(“move Through crosswalk”);
while(isCrosswalk()){
if((millis() – time) >= SENSING_PERIOD){
time = millis();
getIRSensor();
}

MotorA(STRAIGHT, STRAIGHT_SPEED);
MotorB(STRAIGHT, STRAIGHT_SPEED);
delay(100);
}
}
/****************************/

 

IRSensor

/* 프로그램 내용 : bfd-1000 IR센서 관련 함수
*
* getIRSensor IR센서 값 읽어오는 함수
* getIRSensor_Hz 주어진 Hz 속도로 IR센서 값 읽어오는 함수
* showIRSensorChar IR센서 값을 문자로 시리얼모니터로 출력해주는 함수
* showIRSensorValue IR센서 값을 시리얼모니터로 출력해주는 함수
*
*/

void getIRSensor()
{
LeftOut = digitalRead(SS1_LEFT_OUT);
LeftIn = digitalRead(SS2_LEFT_IN);
//Center = digitalRead(SS3_CENTER);
RightIn = digitalRead(SS4_RIGHT_IN);
RightOut = digitalRead(SS5_RIGHT_OUT);
//Bump = digitalRead(CLP_BUMP);
//Near = digitalRead(NEAR);
}

void getIRSensor_Hz(int Hz)
{
if( (millis()-time) % (1000 / Hz) == 0 )
{
getIRSensor();
}
}

void showIRSensorChar()
{
if (LeftOut == 1) {Serial.print(“L “);} else {Serial.print(“- “);}
if (LeftIn == 1) {Serial.print(“l “);} else {Serial.print(“- “);}
//if (Center == 1) {Serial.print(“C “);} else {Serial.print(“- “);}
if (RightIn == 1) {Serial.print(“r “);} else {Serial.print(“- “);}
if (RightOut == 1) {Serial.print(“R “);} else {Serial.print(“- “);}
//if (Bump == 1) {Serial.print(” BUMP!”);} else {Serial.print(” “);}
//if (Near == 1) {Serial.print(” NEAR”);} else {Serial.print(” “);}
Serial.println();
}

void showIRSensorValue()
{
Serial.print(“IRSensor : “);
Serial.print(LeftOut);Serial.print(LeftIn);
Serial.print(RightIn);Serial.print(RightOut);
Serial.println();
}

main_ver3

#include “WiFiEsp.h”
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>
#include “SoftwareSerial.h”

/* Arduino Pin */
#define SS1_LEFT_OUT A1
#define SS2_LEFT_IN A2
#define SS4_RIGHT_IN A3
#define SS5_RIGHT_OUT A4

#define encoderPinA 2
#define encoderPinB 3

#define MotorA1 5
#define MotorA2 6
#define MotorB1 10
#define MotorB2 11
#define TURN_SPEED 200
#define STRAIGHT_SPEED 110

#define STRAIGHT_SPEED_STRONG 200
#define STRAIGHT_SPEED_WEEK 110

#define SENSING_PERIOD 100

/* global variable – IR sensor */
byte LeftOut;
byte LeftIn;
byte RightIn;
byte RightOut;

/* global variable – ServingRobot Direction */
int cmd_direction = 0;
int cnt_isRoad = 0;

/* 서빙로봇 명령 */
const int STOP = 0;
const int LEFT = 1;
const int STRAIGHT = 2;
const int RIGHT = 3;
const int BACK = 4;
const int FORWARD_THROUGH_CROSSWALK = 5;
/* global variable – PID Control */
long encoderPos = 0;
double angle = 0, angle_pre=0;
int time1 = 0;
int value = 0;

double v;
double v_pre = 0;
float Kp = 4.2227;
float Ki = 2.29743;
float Kd = -0.03976;
float Ke = 0.084;

/*
float Kp = 1.674;
float Ki = 0.8239;
float Kd = -0.3584;
float Ke = 0.084;
*/
int k = 0;

double PControl, IControl=0, DControl, PIDControl;
double error, error_pre;
int pwm_in;

/* global variable – WiFi */
long target_table = 0;

/* global variable – Find Table */
int crosswalk[9] = {0, 1, 1, 2, 2, 3, 3, 4, 4};//갈림길 지나야하는 횟수. index 0은 사용하지 않는다.
int turn[9] = {0, LEFT, RIGHT, LEFT, RIGHT, LEFT, RIGHT, LEFT, RIGHT};//회전해야할 때, left로 가야하는지 right로 가야하는지
int crosswalkCnt = 0;//몇 번째 갈림길을 만났는지 체크!
bool isDone = true;

WiFiEspClient client;
MySQL_Connection conn((Client *)&client);
// Create an instance of the cursor passing in the connection
MySQL_Cursor cur = MySQL_Cursor(&conn);
MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);

SoftwareSerial esp(8, 9); // RX, TX
byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(106,10,49,227); // IP of the MySQL *server* here
char user[] = “sexymandoo”; // MySQL user login username
char password[] = “sexymandoo”; // MySQL user login password

char ssid[] = “Sexy_Jaewon”; // 공유기 이름 SSID
char pass[] = “jaewonsexy”; // 공유기 암호 Password
// Sample query
char query[] = “SELECT _table FROM project.orderDB WHERE call_arduino = ’1′ AND serving = ’0′ ;”;
char query2[] = “UPDATE project.orderDB SET serving = ’1′ WHERE call_arduino = ’1′ AND serving = ’0′ ;”;
int status = WL_IDLE_STATUS; // Status
int Hz = 20;
unsigned long time; // 시간 측정을 위한 변수
void setup() {
pinMode(encoderPinA, INPUT_PULLUP);
attachInterrupt(0, doEncoderA, CHANGE);
pinMode(encoderPinB, INPUT_PULLUP);
attachInterrupt(1, doEncoderB, CHANGE);

pinMode(MotorA1, OUTPUT);
pinMode(MotorA2, OUTPUT);
pinMode(MotorB1, OUTPUT);
pinMode(MotorB2, OUTPUT);

Serial.begin(9600);
esp.begin(9600);
time = millis();

WiFi.init(&esp);

if (WiFi.status() == WL_NO_SHIELD)
{
Serial.println(F(“WiFi shield not present”));
while (true);
}

// 와이파이 접속여부 확인
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(F(“Attempting to connect to WPA SSID: “));
Serial.println(ssid);

status = WiFi.begin(ssid, pass);
}

// 와이파이 접속정보
Serial.println(F(“You’re connected to the network”));
Serial.print(F(“SSID: “));
Serial.println(WiFi.SSID());

IPAddress ip = WiFi.localIP();
Serial.print(F(“IP Address: “));
Serial.println(ip);

long rssi = WiFi.RSSI();

Serial.println(F(“Connecting to the server”));
if (conn.connect(server_addr, 3306, user, password)) {
delay(1000);
Serial.println(F(“Connected to the server”));
}
else
Serial.println(F(“Server connection failed.”));
}

void loop() {

if(target_table == 0){
get_table_number();
isDone = false;
crosswalkCnt = 0;
}
if(target_table != 0)
{
lineTracing();
}
if(isDone && (target_table != 0))
{
cur_mem->execute(query2); //MySQL에 서빙 완료를 알림
cur.close();
target_table = 0;
}

//pidControl_Hz(Hz, 30); // 30cm/s 속도로 주행
}

void get_table_number()
{
row_values *row = NULL;
target_table = 0;

delay(1000);

cur_mem = new MySQL_Cursor(&conn);
// query를 실행시켜서 MySQL에서 데이터를 읽어옴
cur_mem->execute(query);
column_names *columns = cur_mem->get_columns();

do {
row = cur_mem->get_next_row();
if (row != NULL) {

target_table = atol(row->values[0]);
}
} while (row != NULL);
// 동적 메모리 할당 지우기
delete cur_mem;

// 결과 출력
Serial.print(F(” 서빙테이블 = “));
Serial.println(target_table);

delay(500);
}

 

 motor

/* 프로그램 내용 : DC Motor 컨트롤하는 함수들
*
* MotorA MotorA를 PWM 전압으로 조절하는 함수
* MotorB MotorB를 PWM 전압으로 조절하는 함수
*
* doEncoderA DC모터 엔코더 카운트 하기 위한 함수
* doEncoderB DC모터 엔코더 카운트 하기 위한 함수
* pos2ang 엔코더로부터 회전 각도 구하는 함수
* pos2ang_Hz 주어진 Hz속도로 회전 각도 구하는 함수
*
*/

void MotorA(int dir, int _speed)
{
if (dir == STRAIGHT)
{
analogWrite(MotorA1, 0);
analogWrite(MotorA2, _speed);
}
else
{
analogWrite(MotorA1, 0);
analogWrite(MotorA2, 0);
}
}

void MotorB(int dir, int _speed)
{
if (dir == STRAIGHT)
{
analogWrite(MotorB1, _speed);
analogWrite(MotorB2, 0);
}
else
{
analogWrite(MotorB1, 0);
analogWrite(MotorB2, 0);
}
}

void doEncoderA()
{
encoderPos += (digitalRead(encoderPinA)==digitalRead(encoderPinB))?1:-1;
}

void doEncoderB()
{
encoderPos += (digitalRead(encoderPinA)==digitalRead(encoderPinB))?-1:1;
}

void pos2ang()
{
angle = -encoderPos/(341.2*4) * 360 /180*3.141592 ;
}

 

 PIDControl

void pidControl_Hz(int Hz, double input_v)
{
double t = 0.1;
pos2ang(); // 회전 각도 측정(라디안)
v = 3.15*(angle-angle_pre)/t;
angle_pre = angle;
error=(input_v-v)*Ke;
PControl=Kp*error;
if(IControl <= 4){IControl+=Ki*error*t;} //Windup 방지
//IControl+=Ki*error*t;
DControl=Kd*(error-error_pre)/t;

PIDControl=PControl+IControl+DControl;

pwm_in=255/6*PIDControl;
if(pwm_in>=255){pwm_in=255;}
else if(pwm_in<=60){pwm_in=60;}

Serial.print(“pwm_in: “); Serial.print(pwm_in);
Serial.print(” v:”); Serial.print(v);
Serial.print(” error:”); Serial.print(error/Ke);
Serial.print(” PIDControl: “); Serial.print(PIDControl);
Serial.print(” P:”); Serial.print(PControl);
Serial.print(” I:”); Serial.print(IControl);
Serial.print(” D:”); Serial.println(DControl);

//Serial.println(v);

MotorA(STRAIGHT, pwm_in);
MotorB(STRAIGHT, pwm_in);
//MotorA(STRAIGHT, -50);
//MotorB(STRAIGHT, -50);

//angle_pre = angle;
v_pre = v;
error_pre = error;
}