2021년 1월 26일 화요일

오픈소스 기반 무인 배송 로봇 Kiwibot 시스템 소개 및 구조 분석

이 글은 오픈소스 기반 무인 배송 로봇 Kiwibot에 대한 시스템 소개 및 구조를 간략히 다룬다. 이 배송 로봇의 초창기 개발 관련 소스코드가 있어, 구조를 간단히 분석하고 나눔한다.

머리말

Kiwibot은 배달 로봇으로 무인 자율로 물건을 배송한다. 

이 로봇은 캘리포니아 버클리 대학에서 시작된 무인자율 로버 기반 딜리버리(delivery) 서비스 스타트업 Kiwi Campus(2016년 설립)에서 오픈소스 프로젝트로 진행되었었다. 2017년 버클리 캘리포니아 대학교에서 시작된 이 회사는 버클리 도시 일부, 팔로 알토 스탠포드 대학교 캠퍼스, 산호세 등에서 서비스를 제공한다.

키위 스타트업은 세 가지 종류 로봇을 사용한다. 로봇은 레스토랑에서 준비된 주문을 수집하고, 주문을 분류하여 배달 지점까지 가져가며, 물건을 운반하는 로버 "키위봇"으로 구성하고 있다. 이 서비스는 주문 후 평균 27분 납품이 가능하다고 한다.

현재 키위봇은 20개의 대학 캠퍼스와 도시에서 운영되고 있으며, 2018년 5월 기준 100,000건 이상의 주문을 처리했다. 이 회사 CEO인 Felipe Chávez는 2018년 11월 MIT 기업가상을 수상했다.

이 중 이 글은 키위봇을 소개한다.

키위봇 시스템 모듈 분석

키위봇은 탐색 및 회피를 위한 6개의 카메라를 장착하고 있고, 여기서 얻은 이미지 데이터를 딥러닝 및 머신러닝을 이용해, 속도 조절, 장애물 회피 등의 동작을 한다. 초기에는 거친 보도블럭 등으로 인해 기구부에서 문제가 있었고, GPS 음역 지역에서 수동 조작 등의 이슈가 있었다고 보고되었다.  

코드를 확인해 보면, ROS(ROBOT OPERATING SYSTEM), NVIDIA TK2, 카메라 이미지 및 딥러닝 기반 비전 기술을 사용한 것을 알 수 있다.

전체적으로 보면, 하드웨어는 NVIDIA TK, 6개 카메라 센서, GPS, ODOMETRY, 통신 모듈을 사용해 데이터를 얻고, 소프트웨어는 ROS기반위에 이미지를 처리하고, 목적지 네비게이션, 속도 및 장애물 처리, 예외사항 알림 등의 구현을 하고 있다. 외부는 거친 보도블럭을 고려한 휠 구조, 물건을 담을 수 있는 컨테이너, HCI기반 메시지를 전달하는 디스플레이를 가진다.

키위봇은 크게 다음과 같은 패키지를 가진다.

  • kiwi_image_pipeline: ROS 이미지 파이프라인을 이용해, 카메라로 부터 데이터를 처리
  • multicamera_stitching: 다중 카메라로 부터 이미지 스티칭(stitching) 처리
  • kiwi_navigation_stack, kiwi_navigation_tools: ROS 주행 탐색 스택. 로봇 이동 경로 정보 생성. 효율적 경로 탐색을 위해 cost map 구현되어 있음.
  • rtabmap_ros: RGBD SLAM 기반 지도 생성 모듈. ROS기반 RTABMAP으로 구현됨. 
  • maplab: open visual inertial mapping 프레임웍. 비쥴얼 관성 맵핑 및 위치 정보 계산 프레임웍. 
  • socketio-server, websocketpp: 서버와 로버 간 통신 처리 부분.
  • kiwibot_dsdl: 키위봇 UAVCAN DSDL 정의 부분
  • 기타: 텐서플로우 딥러닝 처리 모듈, Scikit 라이브러리, robot_localization

소프트웨어 구성 모듈을 확인해 보면, 개별 모듈은 대부분 오픈소스에서 가져왔다. 다만, 키위봇을 동작시키기 위해, 목적에 맞게 각 모듈에서 처리하는 정보를 다른 모듈로 연결하고, 경로 계산 등 실제 현장에서 사용될 때 이슈에 대응하기 위한 최적화 코드들이 추가되어있으며, 키위봇 모니터링 및 조작을 위한 통신부가 구현되어 있음을 알 수 있다. 

ROS 기반 이미지 파이프라인
maplab 기반 주행괘적 및 지역화 처리

소스 모듈을 보면, 초창기 기술 개발에 MYNT EYE 센서 등 여러 장치를 테스트해보았다는 것을 확인할 수 있다. 

소스에서 경로 탐색 및 맵 생성 부분은 로버 개발 시 핵심이 되는 부분이다. 다음은 navigation 패키지를 구성하는 핵심 모듈들이다.  

소스 코드 중 많은 부분은 2010-2020년 사이에 개발된 것들로 대부분 BSD, MIT 라이센스를 가지고 있다. 2017년도 부터 테스트를 시작한 것으로 보아, 그때까지는 이런 모듈들을 대부분 완성하고, 이후에는 주행 테스트를 통해 데이터를 취득하고, 테스트를 통한 주행 모델 등을 최적화하는 데 집중한 것으로 보인다. 안전성을 높이는 노력은 약 2년 정도 진행된 것으로 보인다. 

각 패키지에 대한 상세한 코드는 아래 github에서 확인할 수 있다.


마무리
몇년전 버클리 대학 캠퍼스에서 보았던 배달 로봇 시작품이 이제는 큰 스타트업이 되어, 많은 곳에서 실제 사용되고 있다고 하니, 발전 속도가 정말 빠른것 같다. 그 사이 테스트도 하고, 배터리 화재 관련 문제있어 관련 조치도 있었고, 많은 일을 해낸것 같다. 

실제 사용될 수 있는 기술을 개발한다는 것은 현장에서 실제 운용하며 이렇게 많은 시간, 노력, 시행착오가 필요한 것이다. 이 회사는 오픈소스를 잘 활용하였고, 실 사용 데이터를 얻어 모델을 최적화하기 위해, 전략적으로 회사를 운영하였다. 이런 업체들이 모여 개방적인 실리콘밸리 생태계를 이루고 있는 것이 인상적이다. 
Kiwibot 설립자 및 직원
레퍼런스

추신. 회사 행정에서 좀 떨어지니, 기술, 소스 코드 좀 더 파 볼 수 있었다. 연구할 시간도 좀 많아진듯해 다행. 그나저나, 연구하라고 세금으로 만든 연구소가 과제펀드 관리행정이 주업무, 오히려 연구하기 참 힘든 환경이니 아이러니하다. 선택의 문제이긴 하나, 이런 환경에서는 기술 개발 포기하고 그냥 아웃소싱하는 것이 더 이익이고 합리적이게 된다.

2021년 1월 23일 토요일

저가 IoT 장치 개발을 위한 통합 임베디드 모듈 TTGO WiFi Bluetooth ESP32 개발 방법 및 사용기

이 글은 그 동안 리뷰 미루어 두었던, 통합 무선 모듈 TTGO WiFi Bluetooth ESP32 개발 방법 및 사용기를 간단히 나눔한다. 

이 모듈은 GPIO16, WiFi, Bluetooth, OLED, 충전식 배터리 장치를 모두 포함하고도 겨우 10달러 아래에 구입할 수 있는 ESP32 칩 기반 소형 임베디드 컴퓨터이다. 18650 소형 배터리를 지원하며, 프로그래밍은 아두이노 IDE에서 코딩할 수 있다. 이런 기능을 통해, 손쉽게 저가의 IoT 장치를 개발할 수 있다.

다음 영상은 이 모듈을 이용해 개발한 사례를 보여준다.

개발 사례(TTGO WiFi & Bluetooth Battery ESP32 0.96 inch OLED development tool)
날씨 측정을 위한 웨더스테이션 개발 사례

ESP32 보드
TTGO ESP32에 사용된 메인칩인 ESP32는 Wi-Fi, Bluetooth가 통합된 칩 마이크로 컨트롤러이며, 저가형 저전력 시스템이다. 참고로, TTGO는 www.lilygo.cn 브랜드이며, 중국 심천의 보드 회로 메이커이다. ESP32 시리즈는 Tensilica Xtensa LX6 마이크로 프로세서를 사용해 내장 안테나 스위치, RF, 필터 및 전력 관리 모듈을 포함하고 있다. ESP32는 상하이에 본사를 둔 중국 회사인 Espressif Systems가 개발했으며 TSMC에서 40nm 공정을 사용하여 제조된다. 이 칩은 ESP8266 마이크로 컨트롤러의 후속 제품이다.

ESP32 칩 기반 보드는 주변에 어떤 모듈을 추가했느냐에 따라 다음 그림과 같이 다양한 변종이 존재한다. TTGO WiFi Bluetooth ESP32 모듈은 ESP32 칩 기반 모듈 중 무선 통신 및 OLED 기능을 가지고 있다.

ESP32 모듈의 GPIO 레이아웃 구조는 다음과 같다. 

모듈 스펙
이 모듈은 다음과 같은 형태로 디자인되어 있다. 앞면은 OLED, WiFi, GPIO등이 보이고, 뒤면은 18650배터리를 장착할 수 있도록 되어 있다.
ESP32 보드와 18650 배터리

이 모듈의 자세한 스펙은 다음과 같다. 
  • Micro USB 입력
  • Integrated 18650 충전 지원
  • SDA OLED, SCL은 D1 pin, D2 pin과 연결됨
  • 인터럽트 / pwm / I2C 지원
  • Integrated OLED 지원
  • Input voltage range: 5-12V. Charge current: 500 mA
다음은 18650 배터리 사용 시 주의사항이다. 
If the battery is in the wrong direction, the charging chip will be destroyed.
When you burn a program. Please disconnect the battery and check that the switch is in position.

아두이노 IDE 설정 및 사용 방법
다음 순서로 아두이노 IDE를 설정한다. 우선, 개발보드를 추가해야 하고, 그 다음 라이브러리를 추가해 예제를 실행해 본다.

1. 모듈과 컴퓨터를 USB 케이블로 연결한다. 그리고, FILE > 환경 설정 메뉴에서 다음 링크를 추가한다.

2. 아두이노 IDE에서 Tools > Board > Boards Manager 메뉴를 선택하고 ESP32 를 검색한 후보드를 설치한다. 

BOARD IN ESP32 BY Espressif

3. 다음과 같이 WEMOS LOLIN32 보드를 선택한다. 그리고, 연결된 COM 포트를 선택한다.

4. 아두이노 IDE에서 FILE > EXAMPLE > ESP32 메뉴의 BLUETOOTH 예제를 선택한다.

5. 아두이노 IDE에서 UPLOAD 버튼을 클릭한다. 이때, 보드의 BOOT BUTTON을 누르고 있어야 한다. 모듈의 PORT 0 과 GND를 서로 연결해도 동일하게 동작한다. 만약, 버튼을 누르지 않으면, 다음과 같은 에러가 발생하고, 코드가 모듈로 업로드되지 않는다.

ESP32 Timed out 문제
코드 업로드 장면(Boot 버튼 푸시 후 코드 업로드되면 EN버튼 푸시할 것)

이 문제에 대한 좀 더 상세한 내용은 다음 링크를 참조한다.

6. 코드가 다 업로드되면, BOOT 버튼를 놓고, 모듈의 EN 버튼을 클릭한다. 그럼, 코드가 동작할 것이다. 

7. 아두이노 IDE의 시리얼 모니터 메뉴를 선택하면, 예제 코드에서 출력되는 내용을 확인할 수 있다. 다음과 같이 모듈에서 획득한 BLUETOOTH 스캔 결과를 데이터로 볼 수 있을 것이다. 


동작 결과

다른 ESP32 모듈 예제는 FILE > EXAMPLE > ESP32 메뉴에서 실행해 본다. 

OLED 디스플레이 예제
내장된 디스플레이에 그림을 출력해 본다. 디스플레이 칩은 SSD1306이다. 사용을 위해 다음과 같이 라이브러리를 추가한다.

그리고, 새파일을 만든 후, 다음 소스코드를 복사해 붙여 넣는다.
/*********
  Complete project details at https://randomnerdtutorials.com  
*********/

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

static const uint8_t image_data_Saraarray[1024] = {
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x14, 0x9e, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x36, 0x3f, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x6d, 0xff, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0xfb, 0xff, 0x80, 0x1f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x03, 0xd7, 0xff, 0x80, 0x0f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x07, 0xef, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xdf, 0xff, 0x90, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x0f, 0xbf, 0xff, 0xd0, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x1d, 0x7f, 0xff, 0xd0, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x01, 0x1b, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x02, 0xa7, 0xff, 0xff, 0xc0, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x03, 0xff, 0xc0, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0xff, 0x80, 0x00, 0x0b, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xff, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0x80, 0x07, 0xff, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0f, 0x07, 0xff, 0xf8, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x0e, 0x01, 0xff, 0xc0, 0x38, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x1c, 0x46, 0xff, 0xb1, 0x18, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0x97, 0xff, 0xc0, 0x7a, 0x07, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfe, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0x3f, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfe, 0x01, 0xbf, 0xff, 0xff, 0xff, 0xfe, 0x81, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0xbf, 0xff, 0xff, 0xff, 0xfc, 0x81, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xfc, 0x00, 0xff, 0xff, 0xfe, 0xff, 0xfd, 0x83, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0xbf, 0xff, 0xfe, 0xff, 0xfd, 0x01, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xf0, 0x00, 0x7f, 0xff, 0xff, 0xff, 0xfb, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x3f, 0xff, 0xdc, 0xff, 0xfa, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xd8, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x03, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xd0, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x90, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x02, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xb0, 0x00, 0x0f, 0xf5, 0xff, 0xd7, 0xf8, 0x01, 0xff, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xb0, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x5f, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xa0, 0x00, 0x0f, 0xfb, 0xff, 0xff, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x0f, 0xfd, 0xff, 0xdf, 0xf0, 0x00, 0x3f, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x07, 0xff, 0xff, 0xbf, 0xf0, 0x00, 0x0f, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x07, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x87, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x80, 0x00, 0x03, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x43, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x01, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x73, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfe, 0xe0, 0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x00, 0x7b, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfd, 0xe0, 0x00, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x33, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfd, 0xe0, 0x00, 0x00, 0x3f, 0xff, 0xf8, 0x00, 0x00, 0x27, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0xe0, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x60, 0x00, 0x00, 0x67, 0xff, 0xe0, 0x00, 0x00, 0x1b, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfd, 0x40, 0x00, 0x00, 0xf3, 0xff, 0xc4, 0x00, 0x00, 0x0b, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfe, 0x80, 0x00, 0x00, 0xfc, 0xff, 0x8c, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x7f, 0x3c, 0x3c, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x3f, 0xc0, 0x7c, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff, 
    0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xfc, 0x00, 0x00, 0x03, 0xff, 0xff, 0xff
};
 
void setup() {
  Serial.begin(115200);
  
  // Start I2C Communication SDA = 5 and SCL = 4 on Wemos Lolin32 ESP32 with built-in SSD1306 OLED
  Wire.begin(5, 4);

  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, false)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;);
  }
  delay(2000); // Pause for 2 seconds
 
  // Clear the buffer.
  display.clearDisplay();
  
  // Draw bitmap on the screen
  display.drawBitmap(0, 0, image_data_Saraarray, 128, 64, 1);
  display.display();
}
 
void loop() {
  
}

앞서 했던 것처럼, TTGO ESP32보드에 코드를 업로드한다. 그럼, 다음과 같이 OLED에 출력된 그래픽을 확인할 수 있다.

기타 예제는 다음 github를 참고한다.
충전 및 사용
TTGO 모듈은 충전기가 내장되어 있다. 18650 배터리 (2600mAh)를 테스트해본 결과, OLED LCD를 계속 동작시킨 상태에서 2일정도를 사용할 수 있었다. 

마무리
이 글은 TTGO WiFi Bluetooth ESP32 개발 방법 및 사용기를 간단히 정리해 보았다. 이 모듈은 센서, OLED 디스플레이, WiFi, Bluetooth 등이 모두 통합되어 있어, 관련 기능이 필요할 때 매우 편리하게 개발할 수 있다. 가격도 몇 달러밖에 되지 않아, 사용에 부담이 없다. 다만, 개발 시 자료가 부족하고, 응용 사례가 많지 않은 것이 단점이다. 잘 활용한다면, 저렴한 가격에 괜찬은 IoT 장치를 개발할 수 있을 것이다.

레퍼런스

TTGO WiFi Bluetooth Battery Module 18650 ESP32 0.96 inch OLED Development Tools
TTGO ESP32 OLED DEVELOPMENT TOOL
TTGO Weather station
PlatformIO.org and getting started
ThingPlus OLED SSD1306
ESP32 OLED SSD1306
A fatal error occurred: Timed out waiting for packet content - problem after writing to SPIFFS memory of ESP32

2021년 1월 20일 수요일

Arduino Nano 33 BLE보드와 TinyML 기반 딥러닝 처리 방법 및 사용기

이 글은 Arduino Nano 33 BLE 보드와 TinyML 기반 딥러닝 활용 방법에 대한 글이다. 

이 아두이노 마이크로 컨트롤러는 20달러로 저가 단일칩 독립형 컴퓨터이다. 웨어러블, 드론, 3D 프린터, 장난감, 스마트 플러그, 전기 스쿠터, 세탁기와 같은 장치에 내장된 컴퓨터이다. 이러한 장치를 인터넷으로 연결하면, 사물 인터넷이된다.

아두이노 커뮤니티는 TensorFlow Lite Micro를 새롭게 개발된 Arduino Nano 33 BLE Sense 보드에 도입했다. 이를 사용하면, 다양한 딥러닝 기계학습 어플리케이션을 손가락만한 아두이노 임베디드 보드에서 실행할 수 있다. 

아두이노에 사용하는 보드는 64MHz에서 실행되는 Arm Cortex-M4 마이크로 컨트롤러가 있으며 1MB 플래시 메모리와 256KB RAM이 있다. Arduino Nano 33 BLE Sense 보드는 손가락 크기보다 작다. 참고로, 임베디드 보드 개발시 다음과 같은 문제들로 데이터가 제대로 취득되지 않을 수 있다. 임베디드는 일반 컴퓨터보다 매우 민감하게 작동된다는 점을 고려해야 한다.

  • 소모 전류량 부족: 너무 많은 장치를 연결하지 않는다. 별도 전원을 준비한다.
  • 보드 불량(가끔식 발생함)
  • 회로 연결 오류: 테스터기로 확인해야 함
  • 불완전한 센서 문제: 센서 읽기에 지연, 인터럽트 사용, 보드 강제 리셋 등이 필요할 수 있음
  • 센서 간 충돌: 보드 내 조도 센서는 IMU등 다른 센서와 충돌이 있음. 테스트 필요함
  • 칩 자체 발열로 인한 센서값 오프셋: 캘리브레이션 처리가 필요하거나, 발열을 피할 수 있는 조건을 찾아야 함

이 부분은 아두이노 나노에 대한 많은 리포트에서 확인할 수 있다. 제대로 작동하지 않은 경우, 상세 내용은 다음 링크를 참고한다.

이 글은 아두이노 나노 33 센스 보드를 이용한 딥러닝 처리 방법을 간단히 이야기한다.

아두이노와 TinyML

기계 학습에는 신경망 모델을 마이크로 컨트롤러와 같은 메모리 제약있는 장치에서 실행할 때 사용할 수 있는 기술이 있다. 핵심 단계 중 하나는 부동 소수점을 8 비트 정수로 변환해 양자화하는 것이다. 이를 통해 딥러닝 계산을 더 빠르게 실행할 수 있다. 이런 임베디드 컴퓨터에서 기계학습(ML. Machine Learning) 처리하는 방법 중 하나가 TinyML이다. TinyML은 텐서플로우로 학습된 모델을 경량화해, 메모리 크기가 작은 아두이노 같은 소형 임베디드 컴퓨터에서 실행할 수 있도록 도와준다.

TinyML은 떠오르는 분야이다. 수십억 개의 마이크로 컨트롤러가 모든 종류의 장소에서 다양한 센서와 결합되어, 창의적이고 가치있는 TinyML 애플리케이션을 만들 수 있다. 아래 링크를 방문하면, TinyML 커뮤니티 글, 기술, 내용 및 예제를 확인할 수 있다.

Arduino Nano 33 BLE 기능

이 글을 따라하기 위해 다음과 같은 재료가 필요하다.

프로그래밍하려면 Arduino Web Editor를 사용하거나 Arduino IDE를 설치해야 한다. 

Arduino Nano 33 BLE Sense에는 다양한 온보드 센서가 있다.

  • 음성 – 디지털 마이크
  • 모션 – 9축 IMU (가속도계, 자이로 스코프, 자력계)
  • 환경 – 온도, 습도 및 압력
  • 빛 – 밝기, 색상 및 물체 근접

아울러, Bluetooth LE 연결이 있으므로 노트북, 모바일 앱 또는 기타 BLE 주변 장치로 데이터를 보낼 수 있다.

보드 설명 영상
보드 레이아웃

좀 더 상세한 내용은 다음 링크를 참고한다.

딥러닝 예제 실행하기

이 글 예제는 원래 TensorFlow 블로그에 Sandeep Mistry, Dominic Pajak에 의해 작성되었음을 밝힌다. 이 개발자들은 음성인식, 모션 동작 인식, 카메라 비전 사람 인식 예제를 워크샵했다. 

  • micro_speech : Yes, No 음성 인식
  • magic_wand: 동작 인식
  • person_detection: 사람 인식

딥러닝 음성 인식

첫번째 예제는 간단한 음성 명령 인식 신경망 모델을 아두이노에 업로드할 것이다. 이를 위해, 먼저 아두이노 IDE를 실행하고, 다음과 같이 Arduino Nano 33 BLE 보드와 라이브러리를 설치한다.

아두이노IDE의 도구>보드 매니저 메뉴를 실행해 다음과 같이 BLE 보드를 검색하고 설치한다.

Arduino Nano 33 BLE 보드 설치

보드를 설치한 후, Arduino 보드를 노트북과 연결한다. 그리고, IDE에서 보드와 COM 포트를 선택한다. 
USB 포트 연결
Arduino Nano 33 BLE 보드 및 포트 선택

이제, 스케치>라이브러리 매니저 메뉴를 선택해, Arduino Tensorflow Lite 를 검색해 라이브러리를 설치한다.

라이브러리 설치 후 파일>예제 메뉴에 Arduino Tensorflow 예제 중 micro_speech 예제를 선택하고, 업로드 한다. 아마, 소스를 컴파일하고 보드에 업로드하는 데 시간이 몇분 걸릴것이다.
업로드 중 모습
업로드 성공 모습

이제 툴>시리얼 모니터 메뉴를 실행한다. 그리고, 아두이노 보드를 입에 가까이 대고, Yes, No 단어를 말해보자. 그럼, 다음과 같이 음성을 인식해 Yes, No 텍스트를 출력해 준다. 

음성 딥러닝 모델 실행 모습

동작 제스쳐 인식

이 예제 실행을 위해, Arduino_LSM9DS1 라이브러리를 검색해 설치한다. 파일>예제>Arduino_TensorflowLite 예제에서 magic_wand 를 선택한다. 소스코드가 열리면, 다음과 같이 업로드한다.


성공적으로 업로드되었다면, 다음과 같이, 나노 보드를 흔들어보자. 그럼, 제스쳐에 따라, O, W와 같은 형태가 시리얼 모니터에 출력될 것이다.
제스쳐 인식 결과
제스쳐 실행 예시

사람 인식
이 예제는 ArduCam 카메라가 준비되어야 한다. 
ArduCam (구매)

카메라와 보드간 회로는 다음과 같이 연결한다.
회로 연결 모습(참고)

사람 인식 예제 실행을 위해, 파일>예제>Arduino_TensorflowLite>person_detection을 선택한다. 앞에서 한것처럼, 메뉴에서 업로드 버튼을 클릭한다. 그럼, 사람 인식 딥러닝 모델이 보드에서 실행되어, 다음과 같이 사람을 인식할 수 있다.
person detection 실행 결과

Nano 33과 배터리 연결하는 방법은 다음 자료를 참고하길 바란다.

IMU 센서 데이터 획득

보드에 내장된 IMU 자세 센서를 사용해 그 데이터값을 얻어본다. 
파일>예제>Arduino_LSM9DS1>SimpleGyroscope파일을 열고, 업로드한다. 
다음과 같이 실행되면 성공한 것이다.

OLED 디스플레이 연결

이 나노 보드에는 OLED 디스플레이가 없다. 그러므로, SSD1306 으로 불리는 OLED 디스플레이를 연결할 경우가 가끔 생긴다. 

나노 보드와 연결은 다음과 같이 한다.


아두이노 IDE에서 adafruit_SSD1306 라이브러리를 설치한다.
그리고, 다음과 같은 소스 코드를 업로드해 본다. 

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels

#define OLED_RESET  -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

#define NUMFLAKES     10 // Number of snowflakes in the animation example

#define LOGO_HEIGHT   16
#define LOGO_WIDTH    16
static const unsigned char PROGMEM logo_bmp[] =
{ B00000000, B11000000,
  B00000001, B11000000,
  B00000001, B11000000,
  B00000011, B11100000,
  B11110011, B11100000,
  B11111110, B11111000,
  B01111110, B11111111,
  B00110011, B10011111,
  B00011111, B11111100,
  B00001101, B01110000,
  B00011011, B10100000,
  B00111111, B11100000,
  B00111111, B11110000,
  B01111100, B11110000,
  B01110000, B01110000,
  B00000000, B00110000 };

void setup() {
  Serial.begin(9600);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  display.display();
  delay(2000); // Pause for 2 seconds

  // Clear the buffer
  display.clearDisplay();

  // Draw a single pixel in white
  display.drawPixel(10, 10, SSD1306_WHITE);

  // Show the display buffer on the screen. You MUST call display() after
  // drawing commands to make them visible on screen!
  display.display();
  delay(2000);
  // display.display() is NOT necessary after every single drawing command,
  // unless that's what you want...rather, you can batch up a bunch of
  // drawing operations and then update the screen all at once by calling
  // display.display(). These examples demonstrate both approaches...

  testdrawline();      // Draw many lines
  testscrolltext();    // Draw scrolling text
  testdrawbitmap(); 
}

void loop() {
}

void testdrawline() {
  int16_t i;

  display.clearDisplay(); // Clear display buffer

  for(i=0; i<display.width(); i+=4) {
    display.drawLine(0, 0, i, display.height()-1, SSD1306_WHITE);
    display.display(); // Update screen with each newly-drawn line
    delay(1);
  }
  for(i=0; i<display.height(); i+=4) {
    display.drawLine(0, 0, display.width()-1, i, SSD1306_WHITE);
    display.display();
    delay(1);
  }
  delay(250);
}

void testscrolltext(void) {
  display.clearDisplay();

  display.setTextSize(2); // Draw 2X-scale text
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(10, 0);
  display.println(F("scroll"));
  display.display();      // Show initial text
  delay(100);

  // Scroll in various directions, pausing in-between:
  display.startscrollright(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrollleft(0x00, 0x0F);
  delay(2000);
  display.stopscroll();
  delay(1000);
  display.startscrolldiagright(0x00, 0x07);
  delay(2000);
  display.startscrolldiagleft(0x00, 0x07);
  delay(2000);
  display.stopscroll();
  delay(1000);
}

void testdrawbitmap(void) {
  display.clearDisplay();

  display.drawBitmap(
    (display.width()  - LOGO_WIDTH ) / 2,
    (display.height() - LOGO_HEIGHT) / 2,
    logo_bmp, LOGO_WIDTH, LOGO_HEIGHT, 1);
  display.display();
  delay(1000);
}

OLED display가 동작하면 성공한 것이다. 

나노 BLE 모듈 센서 통합 테스트

앞서 정리한 내용을 응용해, 나노에 장착된 자이로센서, 가속도센서, 중력센서, 온습도 센서 등을 모두 테스트해 본다. 코드는 다음 깃허브에서 다운로드한다. 

업로드 컴파일 중 발생되는 라이브러리 헤더 에러는 앞서 설명한 대로 아두이노 라이브러리 검색 및 추가 기능으로 해결한다.

결과는 다음과 같다. 

이제, 이 나노 보드를 센싱할 건축 공간이나 부재에 부착하고, 데이터를 모아,유무선 통신으로 클라우드 플랫폼에 업로드하면, 데이터 분석을 할 수 있다. 

결론

이 글은 아두이노 나노 33 센스를 통해 TinyML을 간단히 사용해보고, 그 잠재력을 확인해 보았다. 실제 사용해 보면 알겠지만, 아직은 속도나 정확도가 그리 만족스럽지는 못하다. 다만, 손가락 크기의 보드에서 딥러닝 모델을 처리하고, 처리 데이터 결과를 클라우드 플랫폼에 전달할 수 있는 가능성이 많은 상상을 하게 만든다. 

TinyML은 응용목적에 따라 모델을 적절히 최적화한다면, 좀 더 나은 성능을 기대할 수 있을 것이다. 앞으로, 임베디드 보드를 이용한 딥러닝 처리는 더욱 보편화될 것이라 생각한다. 이 글을 통해, 관련 기술 개발 및 응용에 조금이나마 도움이 되었으면 한다.

레퍼런스