commit
4a909f595b
@ -0,0 +1,205 @@
|
|||||||
|
# 위치 추적
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> [Nitya Narasimhan](https://github.com/nitya)의 스케치노트. 사진을 클릭하여 더 크게 보세요
|
||||||
|
|
||||||
|
## 강의 전 퀴즈
|
||||||
|
|
||||||
|
[강의 전 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/21)
|
||||||
|
|
||||||
|
## 도입
|
||||||
|
|
||||||
|
농부로부터 소비자에게 음식을 전달하는 주요 과정은 트럭, 선박, 비행기 또는 기타 상업 운송 차량에 농산물 상자를 적재하고, 음식을 고객에게 직접 전달하거나 중앙 허브 또는 창고로 가공하는 것을 포함합니다. 농장에서 소비자에 이르는 전체 엔드 투 엔드 프로세스는 *supply-chain*이라는 프로세스의 일부입니다. 아래 영상은 애리조나 주립 대학의 W.P. Carey 경영대학원에서 공급망의 아이디어와 공급망이 어떻게 관리되는지에 대하여 더 자세히 설명하는 영상입니다.
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=Mi1QBxVjZAw)
|
||||||
|
|
||||||
|
> 🎥 이미지를 클릭하여 영상을 시청하세요.
|
||||||
|
|
||||||
|
IoT 장치를 추가하면 공급망을 획기적으로 개선하여 물품들이 있는 곳을 관리하고, 운송 및 물품 처리를 더 잘 계획하며 문제에 더 빨리 대응할 수 있습니다.
|
||||||
|
|
||||||
|
트럭과 같은 일련의 차량을 관리할 때는 주어진 시간에 각 차량이 어디에 있는 지 아는 것이 도움이 됩니다. 차량에는 현재 차량의 위치를 IoT 시스템으로 전송하는 GPS 센서가 장착되어 있어 소유자가 위치를 정확히 파악하고, 선택한 경로를 확인하고, 목적지에 언제 도착할지 알 수 있습니다. 대부분의 차량은 WiFi 서비스 범위 밖에서 작동하므로 이러한 종류의 데이터를 전송하기 위해 셀룰러 네트워크를 사용합니다. 때때로 GPS 센서는 전자 로그 북과 같은 더 복잡한 IoT 장치에 내장됩니다. 이 장치들은 운전자들이 근무 시간에 관한 현지 법을 준수하기 위해 트럭이 운송된 기간 또한 추적합니다.
|
||||||
|
|
||||||
|
이번 강의에서는 GPS(Global Positioning System)센서를 사용하여 차량 위치를 추적하는 방법을 배웁니다.
|
||||||
|
|
||||||
|
이번 강의에서 다룰 내용은 다음과 같습니다:
|
||||||
|
|
||||||
|
* [차량과 연결](#차량과-연결)
|
||||||
|
* [지리공간 좌표](#지리공간-좌표)
|
||||||
|
* [Global Positioning Systems (GPS)](#global-positioning-systems-gps)
|
||||||
|
* [GPS 센서 데이터 읽기](#gps-센서-데이터-읽기)
|
||||||
|
* [NMEA GPS 데이터](#nmea-gps-데이터)
|
||||||
|
* [GPS 센서 데이터 디코딩](#gps-센서-데이터-디코딩)
|
||||||
|
|
||||||
|
## 차량과 연결
|
||||||
|
|
||||||
|
IoT는 *연결된 차량*의 함대를 만들어 상품 운송 방식을 변화시키고 있습니다. 이러한 차량은 중앙 IT 시스템에 연결되어 위치 정보 및 기타 센서 데이터를 보고합니다. 일련의 연결 차량을 보유하는 것은 다음과 같은 다양한 이점을 제공합니다.
|
||||||
|
|
||||||
|
* 위치 추적 - 언제든지 차량의 위치를 정확히 파악할 수 있으므로 다음과 같은 이점이 있습니다.
|
||||||
|
|
||||||
|
* 차량이 목적지에 도착하기 직전에 미리 알람을 받아 직원이 물픔을 내릴 준비를 할 수 있습니다.
|
||||||
|
* 도난 차량의 위치 파악이 가능합니다.
|
||||||
|
* 위치 및 경로 데이터를 교통 문제와 결합하여 운송 중간에 차량의 경로를 변경할 수 있습니다.
|
||||||
|
* 세금을 준수합니다. 일부 국가에서는 공공 도로에서 주행하는 차량에 주행 거리 만큼의 요금을 부과합니다. ([New Zealand's RUC](https://www.nzta.govt.nz/vehicles/licensing-rego/road-user-charges/)), 따라서 차량이 공공 도로에서 주행하는 시간과 개인 도로에서 주행하는 시간을 파악하면 세금을 더 쉽게 계산할 수 있습니다.
|
||||||
|
* 고장이 발생할 경우 유지보수 인력을 어디에 파견해야 하는지 파악할 수 있습니다.
|
||||||
|
|
||||||
|
* 운전자 원격 측정 - 운전자가 제한 속도를 준수하고 적절한 속도로 코너링하며 조기에 효율적으로 제동을 걸고 안전하게 주행할 수 있도록 보장합니다. 연결된 차향에는 사고를 기록하는 카메라가 있을 수도 있습니다. 이는 좋은 운전자들에게 할인된 요금을 주는 보험과 연결될 수 있습니다.
|
||||||
|
|
||||||
|
* 운전자 시간 준수 - 운전자가 엔진을 켜고 끄는 시간을 기준으로 확인하여 법적으로 허용된 시간 동안만 운전하도록 보장합니다.
|
||||||
|
|
||||||
|
이러한 이점들은 결합될 수 있습니다 - 예를 들어, 운전자가 허용된 운전 시간 내에 목적지에 도착할 수 없는 경우, 위치 추적과 결합하여 운전자를 재 경로화 할 수 있습니다. 이는 온도 제어 트럭의 온도 데이터는 현재 경로로 인해 차량이 온도 유지를 할 수 없는 경우 차량을 재 구매 할 수 있도록 합니다.
|
||||||
|
|
||||||
|
> 🎓 물품 유통은 농장에서 슈퍼마켓으로 하나 이상의 창고를 통해 상품을 한 장소에서 다른 장소로 운송하는 과정입니다. 한 농부가 토마토 상자를 트럭에 싣고 중앙 창고로 배달한 다음, 슈퍼마켓으로 배달되는 트럭에는 농부의 토마토와 다른 종류의 농산물들이 혼합되어 있습니다..
|
||||||
|
|
||||||
|
차량 추적의 핵심 구성 요소는 GPS입니다. GPS는 지구상 어디에서나 위치를 정확히 파악할 수 있는 센서입니다. 이 강의에서는 GPS 센서를 사용하는 방법부터 시작하여 지구의 위치를 정의하는 방법에 대해 알아봅니다.
|
||||||
|
|
||||||
|
## 지리공간 좌표
|
||||||
|
|
||||||
|
지리공간 좌표는 지구 표면의 점을 정의하는 데 사용되며, 컴퓨터 화면의 픽셀을 그리거나 십자 스티치로 꿰매는 데 좌표를 사용할 수 있는 방법과 유사합니다. 단일 점의 경우 좌표 쌍이 있습니다. 예를 들어, 미국 워싱턴주 레드몬드에 있는 마이크로소프트 캠퍼스는 47.6423109, -122.1390293에 위치해 있습니다.
|
||||||
|
|
||||||
|
### 위도와 경도
|
||||||
|
|
||||||
|
지구는 3차원 원인 구입니다. 이 때문에 점은 원의 기하학적 구조와 마찬가지로 360도로 나누어 정의된다. 위도는 남북의 의치를 측정하고 경도는 동서의 위치를 측정한다.
|
||||||
|
|
||||||
|
> 💁 아무도 원이 360도로 나뉘는 원래의 이유를 알지 못합니다. 위키피디아의 [degree (angle)](https://wikipedia.org/wiki/Degree_(angle))페이지에서 가능성 있는 몇가지 이유들을 다룹니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
위도는 북반구와 남반구를 각각 90°씩 나누며 지구를 돌고 적도와 평행하게 달리는 선을 이용해 측정합니다. 적도는 0도, 북극은 90도, 남극은 -90도, 남극은 -90도입니다.
|
||||||
|
|
||||||
|
경도는 동서로 측정된 위치로 측정됩니다. 경도의 0°원점은 *원초 자오선*이라고 불리며, 1884년에 [영국 그리니치에 있는 영국 왕립 천문대](https://wikipedia.org/wiki/Royal_Observatory,_Greenwich))를 통과하는 북극에서 남극점까지의 선으로 정의되었습니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> 🎓 자오선은 북극점에서 남극점까지 반원을 이루는 가상의 직선이다.
|
||||||
|
|
||||||
|
점의 경도를 측정하려면 본초 자오선에서 해당 점을 통과하는 자오선까지의 적도 주위의 좌표를 측정합니다. 경도는 본초 자오선에서 서경 180도에서 동경 180도까지이다. 180°와 -180°는 같은 점, 즉 반경 또는 180도를 가리킵니다. 이것은 본초 자오선에서 지구 반대편에 있는 자오선이다.
|
||||||
|
|
||||||
|
> 💁 반자오선은 국제일자선과 거의 같은 위치에 있는 것과 혼동해서는 안 됩니다. 그러나 국제일자선은 직선이 아니며 지정학적 경계에 맞게 다향합니다.
|
||||||
|
|
||||||
|
✅ 조사 해 봅시다 : 현재 위치의 위도와 경도를 찾으십시오.
|
||||||
|
|
||||||
|
### Degree, 분 및 초 VS 십진수 Degree
|
||||||
|
|
||||||
|
전통적으로 위도와 경도의 측정은 시간과 거리를 최초로 측정하고 기록한 고대 바빌로니아인들이 사용했던 숫자 체계인 Base-60을 사용하여 수행되었습니다. 여러분은 아마 시간을 60분으로, 분을 60초로 나누는 것을 깨닫지도 못한 채 매일 60진법을 사용할 것입니다.
|
||||||
|
|
||||||
|
경도와 위도는 Degree, 분, 초 단위로 측정되며 1분은 Degree의 1/60, 1초는 1/60분이다.
|
||||||
|
|
||||||
|
예를 들어, 적도에서:
|
||||||
|
|
||||||
|
* 위도 1°는 **111.3km**입니다.
|
||||||
|
* 위도의 1분은 111.3/60 = **1.855km**
|
||||||
|
* 위도의 1초는 1.855/60 = **0.031km**
|
||||||
|
|
||||||
|
1분이라는 표현은 하나의 인용구이고, 1초라는 표현은 이중 인용구입니다. 예를 들어, 2도, 17분, 43초는 2°17'43"로 기록됩니다. 초의 일부는 소수로 주어지는데, 예를 들어 0.0초는 0°0'0.5"입니다.
|
||||||
|
|
||||||
|
컴퓨터는 Base-60에서 작동하지 않으므로 대부분의 컴퓨터 시스템에서 GPS 데이터를 사용할 때 이러한 좌표는 십진법으로 제공됩니다. 예를 들어, 2°17'43"은 2.295277입니다. Degree 기호는 일반적으로 생략됩니다.
|
||||||
|
|
||||||
|
점에 대한 좌표는 항상 "위도, 경도"로 제공되므로 47.6423109,-122.117198의 이전 마이크로소프트 캠퍼스의 예는 다음과 같습니다.
|
||||||
|
|
||||||
|
* 위도 47.6423109(적도에서 북쪽으로 47.6423109도)
|
||||||
|
* 경도 -122.1390293(원초 자오선에서 서쪽으로 122.1390293도)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Global Positioning Systems (GPS)
|
||||||
|
|
||||||
|
GPS 시스템은 사용자의 위치를 찾기 위해 지구 주위를 도는 여러 위성을 사용합니다. 여러분은 아마도 휴대폰의 지도 앱(예: Apple Maps 또는 Google Maps)에서 위치를 찾거나 Uber 또는 Lyft와 같은 놀이기구 호출 앱에서 또는 자동차에서 위성 내비게이션(sat-nav)을 사용할 때 위치를 확인하기 위해 GPS 시스템을 사용했을 것입니다.
|
||||||
|
|
||||||
|
> 🎓 '위성항법'의 위성은 GPS 위성입니다!
|
||||||
|
|
||||||
|
GPS 시스템은 각 위성의 현재 위치와 정확한 타임스탬프로 신호를 보내는 다수의 위성을 가지고 작동합니다. 이러한 신호는 전파를 통해 전송되며 GPS 센서의 안테나에 의해 감지됩니다. GPS 센서가 이러한 신호를 감지하고 현재 시간을 사용하여 신호가 위성에서 센서에 도달하는 데 걸린 시간을 측정합니다. 전파의 속도가 일정하기 때문에 GPS 센서는 전송된 타임스탬프를 사용하여 센서가 위성으로부터 얼마나 떨어져 있는지 알아낼 수 있습니다. 적어도 3개의 위성으로부터 온 데이터를 전송된 위치와 결합함으로써, GPS 센서는 지구상의 위치를 정확히 찾아낼 수 있다.
|
||||||
|
|
||||||
|
> 💁 GPS 센서는 전파를 감지하기 위해 안테나가 필요합니다. 트럭과 자동차에 내장된 안테나는 일반적으로 앞유리나 지붕에 신호를 받을 수 있도록 배치되어 있습니다. 스마트폰이나 IoT 기기와 같은 별도의 GPS 시스템을 사용하는 경우, GPS 시스템이나 전화기에 내장된 안테나가 앞쪽에 장착되는 등 하늘을 바라 볼 수 있도록 해야 합니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
GPS 위성은 센서 위 고정점이 아닌 지구를 돌고 있어 위치정보에는 위도와 경도 뿐만 아니라 해수면 이상 고도도 포함됩니다.
|
||||||
|
|
||||||
|
과거 GPS는 미군이 시행하는 정확도에 한계가 있어, 정확도가 5m 안팎으로 제한되었습니다. 이 제한은 2000년에 사라지고 30cm의 정확도를 허용했습니다. 신호 간섭으로 인해 이 정확도를 얻는 것이 항상 가능한 것은 아닙니다.
|
||||||
|
|
||||||
|
✅ 스마트폰이 있으면 지도 앱을 실행하여 위치가 얼마나 정확한지 확인해보세요. 전화기가 더 정확한 위치를 얻기 위해 여러 위성을 감지하는 데 짧은 시간이 걸릴 수 있습니다.
|
||||||
|
|
||||||
|
> 💁 이 위성들은 믿을 수 없을 정도로 정확한 원자 시계를 포함하고 있지만, 그것들은 지구의 원자 시계에 비해 하루에 38마이크로초(0.0000038초)씩 표류하는데, 이는 아인슈타인의 특수 상대성 이론과 일반 상대성 이론에 의해 예측된 속도가 증가함에 따라 시간이 느려지기 때문입니다. - 위성들은 지구의 자전보다 더 빨리 이동합니다. 이 드리프트는 특수 상대성 이론과 일반 상대성 이론의 예측을 증명하는 데 사용되어 왔으며 GPS 시스템의 설계에서 조정되어야합니다. 문자 그대로 GPS 위성에서는 시간이 더 느리게 흐릅니다.
|
||||||
|
|
||||||
|
GPS 시스템은 미국, 러시아, 일본, 인도, 유럽연합, 중국을 포함한 많은 국가와 정치 연합에 의해 개발되고 배치되었습니다. 최신 GPS 센서는 이러한 시스템 대부분에 연결하여 더 빠르고 정확한 수정을 할 수 있습니다.
|
||||||
|
|
||||||
|
> 🎓 각 비군사적 사역에서의 위성그룹을 constellations라고 부릅니다.
|
||||||
|
>
|
||||||
|
## GPS 센서 데이터 읽기
|
||||||
|
|
||||||
|
대부분의 GPS 센서는 UART를 통해 GPS 데이터를 전송합니다.
|
||||||
|
|
||||||
|
> ⚠️ UART는[project 2, lesson 2](../../../../2-farm/lessons/2-detect-soil-moisture/README.md#universal-asynchronous-receiver-transmitter-uart)에서 다루었습니다. 필요한 경우 해당 강의를 참조하세요.
|
||||||
|
|
||||||
|
IoT 장치의 GPS 센서를 사용하여 GPS 데이터를 가져올 수 있습니다.
|
||||||
|
|
||||||
|
### 작업 - GPS 센서를 연결하고 GPS 데이터를 읽어봅시다.
|
||||||
|
|
||||||
|
IoT 장치를 사용하여 GPS 데이터를 읽으려면 관련 가이드를 참조하십시오. :
|
||||||
|
|
||||||
|
* [Arduino - Wio Terminal](../wio-terminal-gps-sensor.md)
|
||||||
|
* [Single-board computer - Raspberry Pi](../pi-gps-sensor.md)
|
||||||
|
* [Single-board computer - Virtual device](../virtual-device-gps-sensor.md)
|
||||||
|
|
||||||
|
## NMEA GPS 데이터
|
||||||
|
|
||||||
|
당신이 코드를 실행했을 때, 당신은 출력에서 횡설수설하는 것처럼 보일 수 있는 것을 보았을 것입니다. 이것은 사실 표준 GPS 데이터이고, 모두 의미가 있습니다.
|
||||||
|
|
||||||
|
GPS 센서는 NMEA 0183 표준을 사용하여 NMEA 메시지로 데이터를 출력합니다. NMEA는 해양 전자 간 통신 표준을 설정하는 미국의 무역 조직인 [National Marine Electronics Association](https://www.nmea.org)의 약자입니다.
|
||||||
|
|
||||||
|
> 💁 이 표준은 독점적이며 최소 미화 2,000 달러에 판매되지만, 이에 대한 충분한 정보는 대부분의 표준이 리버스 엔지니어링되었으며 오픈 소스 및 기타 비상업 코드에서 사용될 수 있습니다.
|
||||||
|
|
||||||
|
이 메시지는 텍스트 기반입니다. 각 메시지는 `$` 문자로 시작하는 *문장*으로 구성되며, 그 다음으로 메시지의 소스를 나타내는 2개의 문자(예: 미국 GPS 시스템의 경우 GP, 러시아 GPS 시스템의 경우 GN), 메시지 유형을 나타내는 3개의 문자로 구성됩니다. 메시지의 나머지 부분은 쉼표로 구분된 필드로, 새 줄 문자로 끝납니다.
|
||||||
|
|
||||||
|
수신할 수 있는 메시지 유형은 다음과 같습니다.:
|
||||||
|
|
||||||
|
| 유형 | 설명 |
|
||||||
|
| ---- | ----------- |
|
||||||
|
| GGA | GPS 센서의 위도, 경도, 고도를 포함한 GPS 수정 데이터와 이 수정을 계산하기 위해 볼 수 있는 위성의 수. |
|
||||||
|
| ZDA | 현지 시간대를 포함한 현재 날짜 및 시간 |
|
||||||
|
| GSV | 보기에 있는 위성의 세부 정보 - GPS 센서가 신호를 감지할 수 있는 위성으로 정의됩니다. |
|
||||||
|
|
||||||
|
> 💁 GPS 데이터에는 타임스탬프가 포함되어 있으므로 IoT 장치는 NTP 서버나 내부 실시간 시계에 의존하지 않고 GPS 센서에서 필요한 경우 시간을 얻을 수 있습니다.
|
||||||
|
|
||||||
|
GGA 메시지는 방향을 나타내는 단일 문자와 함께 `(dd)dmm.mmm` 형식을 사용하는 현재 위치를 포함합니다. 형식의 `d`는 도, `m`은 분, 초는 분 단위입니다. 예를 들어, 2°17'43"은 217.7166667 - 2도, 17.7166667분입니다.
|
||||||
|
|
||||||
|
The direction character can be `N` or `S` for latitude to indicate north or south, and `E` or `W` for longitude to indicate east or west. For example, a latitude of 2°17'43" would have a direction character of `N`, -2°17'43" would have a direction character of `S`.
|
||||||
|
방향 문자는 북쪽이나 남쪽을 나타내는 위도의 경우 `N`이나 `S`, 동쪽이나 서쪽을 나타내는 경도의 경우 `E`나 `W`가 될 수 있습니다. 예를 들어, 2°17'43"의 위도는 `N`의 방향 문자를 가지며, -2°17'43"의 방향 문자는 `S`의 방향 문자를 갖는다.
|
||||||
|
|
||||||
|
예시 NMEA 문장 `$GNGGA, 020604.001, 4738.538654, N, 12208.341758, W, 1,3,164.7, M, -17.1, M, *67`
|
||||||
|
|
||||||
|
* 위도 부분은 `4738.538654,N`으로 소수점에서 47.6423109로 변환된다. `4738.538654`는 47.6423109, 방향은 `N`(북)이므로 양위도이다.
|
||||||
|
|
||||||
|
* 경도 부분은 -122.1390293(10진수)으로 환산한 `12208.341758,W`이다. `12208.341758`은 122.1390293°이고 방향은 `W`(서쪽)이므로 음경이다.
|
||||||
|
|
||||||
|
## GPS 센서 데이터 디코딩
|
||||||
|
|
||||||
|
원시 NMEA 데이터를 사용하는 것보다 더 유용한 형식으로 디코딩하는 것이 좋습니다. 원시 NMEA 메시지에서 유용한 데이터를 추출하는 데 사용할 수 있는 여러 오픈 소스 라이브러리가 있습니다.
|
||||||
|
|
||||||
|
### 작업 - GPS 센서 데이터를 디코딩 해 봅시다
|
||||||
|
|
||||||
|
IoT 장치를 사용하여 관련 가이드를 통해 GPS 센서 데이터를 디코딩합니다.:
|
||||||
|
|
||||||
|
* [Arduino - Wio Terminal](../wio-terminal-gps-decode.md)
|
||||||
|
* [Single-board computer - Raspberry Pi/Virtual IoT device](../single-board-computer-gps-decode.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 도전
|
||||||
|
|
||||||
|
여러분만의 NMEA 디코더를 쓰세요! NMEA 문장을 해독하기 위해 타사 라이브러리에 의존하는 대신, 여러분은 NMEA 문장에서 위도와 경도를 추출하기 위해 여러분만의 디코더를 작성할 수 있을까요?
|
||||||
|
|
||||||
|
## 강의 후 퀴즈
|
||||||
|
|
||||||
|
[강의 후 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/22)
|
||||||
|
|
||||||
|
## 복습 및 독학
|
||||||
|
|
||||||
|
* 지리공간 좌표에 대한 자세한 내용은 [Wikipedia의 지리 좌표 시스템 페이지](https://wikipedia.org/wiki/Geographic_coordinate_system))를 참조하세요.
|
||||||
|
* [위키피디아의 프라임 자오선 페이지](https://wikipedia.org/wiki/Prime_meridian#Prime_meridian_on_other_planetary_bodies)에서 지구 외 다른 천체의 프라임 자오선에 대해 자세히 읽어보세요.
|
||||||
|
* EU, 일본, 러시아, 인도 및 미국과 같은 다양한 세계 정부 및 정치 연합의 다양한 GPS 시스템을 찾아보세요.
|
||||||
|
|
||||||
|
## 과제
|
||||||
|
|
||||||
|
[다른 GPS 데이터 조사](../assignment.md)
|
@ -0,0 +1,468 @@
|
|||||||
|
# 위치 데이터 저장
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> [Nitya Narasimhan](https://github.com/nitya)의 스케치노트. 크게 보려면 클릭하세요.
|
||||||
|
|
||||||
|
## 강의 전 퀴즈
|
||||||
|
|
||||||
|
[강의 전 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/23)
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
지난 강의에서는 위치 정보를 캡처하기 위한 GPS 센서 사용법에 대해 배웠습니다. 이 데이터를 사용하여 음식을 실은 트럭으 위치와 이동 경로를 시각화 하기 위해서는 이 데이터가 클라우드의 IoT 서비스로 전송하고 어딘가에 저장해야 합니다.
|
||||||
|
|
||||||
|
이 강의에서는 IoT 데이터를 저장하는 다양한 방법과 서버리스 코드를 이용하여 IoT 서비스의 데이터를 저장하는 방법에 대해 배웁니다.
|
||||||
|
|
||||||
|
이 강의에서는 다음을 배웁니다:
|
||||||
|
|
||||||
|
- [정형 및 비정형 데이터](#정형-및-비정형-데이터)
|
||||||
|
- [IoT Hub로 GPS 데이터 전송하기](#IoT-Hub로-GPS-데이터-전송하기)
|
||||||
|
- [Hot, Warm, Cold 경로](#hot,-warm-cold-경로)
|
||||||
|
- [서버리스 코드를 이용한 GPS 이벤트 처리](#서버리스-코드를-이용한-GPS-이벤트-처리)
|
||||||
|
- [Azure Storage 계정](#Azure-Storage-계정)
|
||||||
|
- [서버리스 코드와 저장소 연결하기](#서버리스-코드와-저장소-연결하기)
|
||||||
|
|
||||||
|
## 정형 및 비정형 데이터
|
||||||
|
|
||||||
|
컴퓨터 시스템은 데이터를 다루고, 이 데이터는 다양한 모양과 크기로 제공됩니다. 단일 숫자에서 많은 양의 텍스트, 비디오 및 이미지, IoT 데이터까지 다양합니다. 데이터는 _정형 데이터_ 와 _비정형 데이터_ 두 가지 중 하나로 나눌 수 있습니다.
|
||||||
|
|
||||||
|
- **정형 데이터**는 변경되지 않는 잘 정의되고 엄격한 구조를 가진 데이터로, 일반적으로 관계가 있는 데이터 테이블에 매핑됩니다. 이름, 생년월일, 주소를 포함한 개인 정보를 예로 들 수 있습니다.
|
||||||
|
|
||||||
|
- **비정형 데이터** 는 잘 정의되고 엄격한 구조가 없는 데이터로, 자주 구조가 변경될 수 있는 데이터입니다. 문서나 스프레드 시트와 같은 것을 예로 들 수 있습니다.
|
||||||
|
|
||||||
|
✅ 조사해봅시다 : 정형 데이터와 비정형 데이터의 다른 예를 생각해 볼 수 있습니까?
|
||||||
|
|
||||||
|
> 💁 구조화는 되었지만 고정된 테이블에 맞지 않는 반정형 데이터도 존재합니다.
|
||||||
|
|
||||||
|
IoT 데이터는 주로 비정형 데이터로 간주됩니다.
|
||||||
|
|
||||||
|
대규모 상업 농장의 차량에 IoT 장치를 추가한다고 생각해봅시다. 차량 유형에 따라 다른 장치를 사용하기를 원할 것입니다. 예를 들어:
|
||||||
|
|
||||||
|
- 트랙터와 같은 농업용 차량의 경우 올바른 밭에서 동작하는지 확인하기 위한 GPS 데이터가 필요합니다.
|
||||||
|
- 식량을 창고로 나르는 배달 트럭의 경우 GPS 데이터는 물론이고 운전자가 안전하게 운전할 수 있도록 속도 및 가속 데이터를 제공하고, 운전자 식별 및 시작/정지 데이터를 제공하여 운전자가 근무 시간에 대한 현지 법률을 준수하도록 보장해야 합니다.
|
||||||
|
- 냉장 트럭의 경우 음식이 너무 뜨겁거나 차갑지 않고 운송 중에 상하지 않도록 온도 데이터도 필요합니다.
|
||||||
|
|
||||||
|
이러한 데이터는 지속적으로 변합니다. 예를 들어, 트럭 운전실에 IoT 장치가 있다면 트레일러가 변경된다면 전송하는 데이터가 변경될 수 있습니다. 예를 들어 냉장 트레일러를 사용할 때만 온도 데이터를 전송합니다.
|
||||||
|
|
||||||
|
✅ 다른 캡처되는 IoT 데이터 어떤 것이 있을까요? 트럭이 운반할 수 있는 화물의 종류와 유지 관리 데이터에 대해 생각해 보십시오.
|
||||||
|
|
||||||
|
이 데이터는 차량에 따라 변하지만 처리를 위해 모두 동일한 IoT 서비스로 전송됩니다. IoT 서비스는 이 비정형 데이터를 검색하거나 분석할 수 있는 방식으로 저장하면서 이 데이터와 다른 구조로도 작동할 수 있어야 합니다.
|
||||||
|
|
||||||
|
### SQL vs NoSQL 스토리지
|
||||||
|
|
||||||
|
데이터베이스는 데이터를 저장하고 쿼리할 수 있는 서비스입니다. 데이터 베이스는 SQL과 NoSQL 2개의 타입으로 나뉩니다.
|
||||||
|
|
||||||
|
#### SQL 데이터베이스
|
||||||
|
|
||||||
|
첫 번째 데이터베이스는 관계형 데이터베이스 관리 시스템(RDBMS) 또는 관계형 데이터베이스입니다. 이는 데이터를 추가, 제거, 업데이트 또는 쿼리하기 위해 SQL(Structured Query Language)을 사용하여 SQL 데이터베이스라고도 합니다. 이러한 데이터베이스는 스프레드시트와 유사한 잘 정의된 데이터 테이블 집합인 스키마로 구성됩니다. 각 테이블에는 여러 개의 명명된 열이 있습니다. 데이터를 삽입할 때 테이블에 행을 추가하여 각 열에 값을 넣습니다. 이렇게 하면 데이터가 매우 엄격한 구조로 유지됩니다. 열을 비워 둘 수 있지만 새 열을 추가하려면 데이터베이스에서 이 작업을 수행하여 기존 행의 값을 채워야 합니다. 이러한 데이터베이스는 한 테이블이 다른 테이블과 관계를 가질 수 있다는 점에서 관계형이라고 합니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
예를 들어, 사용자 개인 정보를 테이블에 저장한 경우 사용자 이름과 주소가 포함된 테이블의 행에 사용되는 사용자당 내부 고유 ID가 있을 수 있습니다. 그런 다음 해당 사용자에 대한 구매 정보와 같은 다른 세부 정보를 다른 테이블에 저장하려는 경우, 해당 사용자 ID에 대한 하나의 열이 새 테이블에 저장됩니다. 사용자를 조회할 때 사용자 ID를 사용하여 한 테이블에서 사용자의 개인 정보를 가져오고 다른 테이블에서 구입한 사용자의 정보를 가져올 수 있습니다.
|
||||||
|
SQL 데이터베이스는 정형 데이터를 저장하고 데이터와 스키마가 매치하는지 확인하고자 하는 경우에 이상적입니다.
|
||||||
|
|
||||||
|
✅ 이전에 SQL을 사용해본 적이 없다면 잠시 시간을 내어 [SQL page on Wikipedia](https://wikipedia.org/wiki/SQL)에서 읽어보세요.
|
||||||
|
|
||||||
|
잘 알려진 SQL 데이터베이스로는 Microsoft SQL Server, MySQL, PostgreSQL이 있습니다.
|
||||||
|
|
||||||
|
✅ 조사해봅시다 : 이러한 몇몇 SQL 데이터베이스 및 기능에 대해 읽어봅시다.
|
||||||
|
|
||||||
|
#### NoSQL 데이터베이스
|
||||||
|
|
||||||
|
NoSQL 데이터베이스는 SQL 데이터베이스와 동일한 엄격한 구조를 가지고 있지 않기 때문에 NoSQL이라 불립니다. 또한 문서와 같은 구조화되지 않은 데이터를 저장할 수 있으므로 문서 데이터베이스라고도 불립니다.
|
||||||
|
|
||||||
|
> 💁 이름과는 무관하게 일부 NoSQL 데이터베이스에서는 SQL을 사용하여 데이터를 쿼리할 수 있습니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
NoSQL 데이터베이스에는 데이터 저장 방법을 제한하는 미리 정의된 스키마가 없으며, 대신 일반적으로 JSON 문서를 사용하여 비정형 데이터를 삽입할 수 있습니다. 이러한 문서는 컴퓨터의 파일과 유사하게 폴더로 구성할 수 있습니다. 각 문서는 다른 문서와 다른 필드를 가질 수 있습니다. 예를 들어, 농장 차량의 IoT 데이터를 저장하는 경우 일부는 가속도계 및 속도 데이터를 위한 필드를 가질 수 있고, 다른 일부는 트레일러의 온도를 위한 필드를 가질 수 있습니다. 운반되는 농산물의 무게를 추적하기 위해 척도가 내장된 트럭과 같은 새로운 트럭 유형을 추가하는 경우, IoT장치가 해당 새로운 필드를 추가할 수 있고 그것은 데이터베이스를 변경하지 않고 저장될 수 있습니다.
|
||||||
|
|
||||||
|
잘 알려진 NoSQL 데이터베이스에는 Azure CosmosDB, MongoDB, CouchDB가 있습니다.
|
||||||
|
|
||||||
|
✅ 조사해봅시다 : 이러한 몇몇 NoSQL 데이터베이스 및 기능에 대해 읽어봅시다.
|
||||||
|
이 강의에서는 IoT 데이터를 저장하기 위해 NoSQL을 사용합니다.
|
||||||
|
|
||||||
|
## IoT Hub로 GPS 데이터 전송하기
|
||||||
|
|
||||||
|
지난 강의에서는 IoT 장치에 연결된 GPS 센서로부터 GPS 데이터를 캡처했습니다. IoT 데이터를 클라우드에 저장하기 위해서 그것을 IoT 서비스로 보내야합니다. 이전 프로젝트에서 사용한 것과 동일한 IoT 클라우드 서비스인 Azure IoT Hub를 다시 한 번 사용하게 됩니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 작업 - GPS 데이터를 IoT Hub로 전송하기
|
||||||
|
|
||||||
|
1. free 티어를 사용해서 새로운 IoT Hub를 생성합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요하다면 [프로젝트 2의 Lesson 4에서 IoT Hub 생성을 위한 지침](../../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#create-an-iot-service-in-the-cloud) 을 참조할 수 있습니다.
|
||||||
|
|
||||||
|
새로운 리소스 그룹을 생성해야합니다. 새 리소스 그룹을 `gps-sensor`라고 지정하고, 새로운 IoT Hub의 이름을 `gps-sensor-<your name>`과 같이 `gps-sensor`에 기반한 고유한 이름으로 지정합니다.
|
||||||
|
|
||||||
|
> 💁 이전 프로젝트에서 사용한 IoT Hub를 가지고 있다면 그것을 재사용해도 좋습니다. 다른 서비스를 만들 때 이 IoT Hub의 이름과 해당 서비스가 속한 리소스 그룹을 사용해야 합니다.
|
||||||
|
|
||||||
|
1. IoT Hub에 새 장치를 추가합니다. 해당 장치를 `gps-sensor`라 부릅니다. 장치의 연결 문자열을 가져옵니다.
|
||||||
|
|
||||||
|
1. 이전 단계의 장치 연결 문자열을 사용하여 GPS 데이터를 새로운 IoT 허브로 전송하도록 디바이스 코드를 업데이트합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요한 경우 [프로젝트 2의 lesson 4에서 IoT와 장치를 연결하는 방법](../../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#connect-your-device-to-the-iot-service)을 참조할 수 있습니다.
|
||||||
|
|
||||||
|
1. GPS data를 전송할 때 다음 형식의 JSON으로 보냅니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"gps" :
|
||||||
|
{
|
||||||
|
"lat" : <latitude>,
|
||||||
|
"lon" : <longitude>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
1. GPS 데이터를 1분마다 보내서 일일 메세지 할당량을 모두 사용하지 않도록 합니다.
|
||||||
|
|
||||||
|
Wio Terminal을 사용한다면 필요한 라이브러리들을 모두 추가하고 NTP serer에서 사용하여 시간을 설정해야 합니다. 또한 코드는 지난 수업의 기존 코드를 사용하여 GPS 위치를 보내기 전에 직렬 포트에서 모든 데이터를 읽었는지 확인해야 합니다. 다음 코드를 사용하여 JSON 문서를 생성합니다:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
DynamicJsonDocument doc(1024);
|
||||||
|
doc["gps"]["lat"] = gps.location.lat();
|
||||||
|
doc["gps"]["lon"] = gps.location.lng();
|
||||||
|
```
|
||||||
|
|
||||||
|
가상 IoT 장치를 사용하는 경우 가상 환경을 사용하여 필요한 라이브러리들을 모두 설치해야합니다.
|
||||||
|
|
||||||
|
Raspberry Pi나 가상 IoT 장치 모두 이전 강의의 기존 코드를 사용하여 위도 및 경도 값을 가져온 후 다음 코드를 사용하여 올바른 JSON 형시으로 전송합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
message_json = { "gps" : { "lat":lat, "lon":lon } }
|
||||||
|
print("Sending telemetry", message_json)
|
||||||
|
message = Message(json.dumps(message_json))
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💁 해당 코드는 [code/wio-terminal](../code/wio-terminal), [code/pi](../code/pi) or [code/virtual-device](../code/virtual-device) 폴더에 있습니다.
|
||||||
|
> 디바이스 코드를 실행하고 CLI 명령 을 사용하여 메시지가 IoT Hub로 흘러들어가고 있는지 확인합니다 .
|
||||||
|
|
||||||
|
## Hot, Warm, Cold 경로
|
||||||
|
|
||||||
|
IoT 장치에서 클라우드로 전송되는 데이터들이 항상 실시간으로 처리되는 것은 아닙니다. 어떤 데이터는 실시간 처리가 필요하고, 다른 데이터는 짧은 시간 뒤에 처리될 수 있으며, 또 다른 데이터는 훨씬 더 나중에 처리되어도 됩니다. 서로 다른 시간에 데이터를 처리하는 서로 다른 서비스로의 데이터 흐름을 Hot 경로, Warm 경로, Cold 경로라고 합니다.
|
||||||
|
|
||||||
|
### Hot 경로
|
||||||
|
|
||||||
|
Hot 경로는 실시간으로 처리되거나 거의 실시간에 근접하게 처리되어야 하는 데이터를 말합니다. 차량이 차고에 접근하고 있거나 냉장 트럭의 온도가 너무 높다는 경고를 받는 등의 경고에 Hot 경로 데이터를 사용할 수 있습니다.
|
||||||
|
|
||||||
|
Hot 경로 데이터를 사용하기 위해서 코드는 클라우드 서비스에서 이벤트를 수신하는 즉시 이벤트에 응답합니다.
|
||||||
|
|
||||||
|
### Warm 경로
|
||||||
|
|
||||||
|
Warm 경로는 수신 후 짧은 시간 동안 보고 또는 단기 분석같은 처리할 수 있는 데이터를 말합니다. 전날 수집된 데이터를 사용하는 차량 주행 거리에 대한 일일 보고서에 웜 경로 데이터를 사용할 수 있습니다.
|
||||||
|
Warm 경로 데이터는 빠르게 액세스할 수 있는 일종의 스토리지 내부의 클라우드 서비스에서 수신한 후 저장됩니다.
|
||||||
|
|
||||||
|
### Cold 경로
|
||||||
|
|
||||||
|
Cold 경로는 과거 데이터를 말하며 데이터를 장기간 저장하여 필요할 때마다 처리합니다. 예를 들어, Cold 경로를 사용하여 차량의 연간 주행 거리 보고서를 얻거나, 경로 분석을 실행하여 연료 비용을 절감할 수 있는 최적의 경로를 찾을 수 있습니다.
|
||||||
|
|
||||||
|
Cold 경로 데이터는 절대 변경되지 않고 빠르고 쉽게 쿼리할 수 있는 대량의 데이터를 저장하도록 설계된 데이터베이스인 데이터 웨어하우스에 저장됩니다. 일반적으로 클라우드 애플리케이션에서는 매일, 매주 또는 매월 정기적으로 실행되어 Warm 경로 스토리지에서 데이터 웨어하우스로 데이터를 이동하는 정기적인 작업을 수행합니다.
|
||||||
|
|
||||||
|
✅ 이 강의에서 지금까지 캡처한 데이터에 대해 생각해 봅시다. Hot 경로, Warm 경로, Cold 경로 중 어떤 데이터 입니까?
|
||||||
|
|
||||||
|
## 서버리스 코드를 이용한 GPS 이벤트 처리
|
||||||
|
|
||||||
|
데이터가 IoT 허브로 유입되면 서버리스 코드를 작성하여 Event-Hub와 호환되는 엔드포인트에 게시된 이벤트를 수신 대기할 수 있습니다. 이 경로는 Warm 경로입니다. 이 데이터는 다음 레슨에서의 여정 보고를 위해 저장되고 사용됩니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 작업 - 서버리스 코드를 이용하여 GPS 이벤트 처리하기
|
||||||
|
|
||||||
|
1. Azure Functions CLI를 사용하여 Azure Functions App을 생성합니다. 파이썬 런타임을 사용하여 `gps-trigger` 폴더 안에에 만들고 Functions App 프로젝트 이름에도 동일한 이름을 사용합니다. 이 작업에 사용할 가상 환경을 생성해야 합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요한 경우 [프로젝트 2의 Lesson 5에서 Azure Functions Project 생성을 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-a-serverless-application)을 참조할 수 있습니다.
|
||||||
|
|
||||||
|
1. IoT Hub의 Event Hub 호환 엔드포인트를 사용하는 IoT Hub 이벤트 트리거를 추가합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요한 경우 [프로젝트 2의 Lesson 5에서 IoT Hub 이벤트 트리거 생성을 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-an-iot-hub-event-trigger)을 참조할 수 있습니다.
|
||||||
|
|
||||||
|
1. `local.settings.json` 파일에서 Event Hub 호환 엔드포인트 연결 문자열을 설정하고 `function.json` 파일에서 해당 항목에 대한 키를 사용합니다.
|
||||||
|
|
||||||
|
1. Azurite app을 로컬 저장 에뮬레이터로 사용합니다.
|
||||||
|
|
||||||
|
1. functions app을 실행하여 GPS 장치에서 이벤트를 수신하고 있는지 확인합니다. IoT 장치도 실행 중이고 GPS 데이터를 전송하고 있는지 확인하십시오.
|
||||||
|
```output
|
||||||
|
Python EventHub trigger processed an event: {"gps": {"lat": 47.73481, "lon": -122.25701}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Azure Storage 계정
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Azure Storage 계정은 다양한 방식으로 데이터를 저장할 수 있는 범용 스토리지 서비스입니다. 데이터를 Blob, 큐, 테이블 또는 파일로 동시에 저장할 수 있습니다.
|
||||||
|
|
||||||
|
### Blob 스토리지
|
||||||
|
|
||||||
|
_Blob_ 이라는 단어는 이진 대형 개체를 의미하지만, 구조화되지 않은 모든 데이터를 가리키는 용어가 되었습니다. IoT 데이터가 포함된 JSON 문서에서 이미지 및 동영상 파일에 이르기까지 모든 데이터를 Blob 스토리지에 저장할 수 있습니다. Blob 스토리지에는 관계형 데이터베이스의 테이블과 유사하게 데이터를 저장할 수 있는 bucket이라는 _컨테이너_ 개념이 있습니다. 이러한 컨테이너에는 Blob을 저장할 하나 이상의 폴더가 있을 수 있으며, 각 폴더에는 파일이 컴퓨터 하드 디스크에 저장되는 방식과 유사하게 다른 폴더가 포함될 수 있습니다.
|
||||||
|
|
||||||
|
이번 강의에서는 IoT 데이터를 저장하기 위해 Blob 스토리지를 사용합니다.
|
||||||
|
|
||||||
|
✅ 조사하기: [Azure Blob Storage](https://docs.microsoft.com/azure/storage/blobs/storage-blobs-overview?WT.mc_id=academic-17441-jabenn)에 대해 읽어보세요.
|
||||||
|
|
||||||
|
### Table 스토리지
|
||||||
|
|
||||||
|
Table 스토리지는 반정형 데이터를 저장할 수 있습니다. Table 스토리지는 실제로 NoSQL 데이터베이스이므로 미리 정의된 테이블 집합이 필요하지 않지만 각 행을 정의하는 고유 키를 사용하여 하나 이상의 테이블에 데이터를 저장하도록 설계되었습니다.
|
||||||
|
|
||||||
|
✅ 조사하기: [Azure Table Storage](https://docs.microsoft.com/azure/storage/tables/table-storage-overview?WT.mc_id=academic-17441-jabenn)에 대해 읽어보세요.
|
||||||
|
|
||||||
|
### Queue 스토리지
|
||||||
|
|
||||||
|
Queue 스토리지를 사용하면 최대 64KB 크기의 메시지를 큐에 저장할 수 있습니다. Queue의 뒤에 메시지를 추가하고 앞에서 읽어나갈 수 있습니다. Queue는 저장 공간이 있는 한 메시지를 무기한 저장하므로 메시지를 장기간 저장하고 필요할 때 읽을 수 있습니다. 예를 들어 GPS 데이터를 처리하기 위해 매달 작업을 실행하려는 경우 한 달 동안 매일 작업을 Queue에 추가한 다음 월말에 모든 메시지를 Queue에서 처리할 수 있습니다.
|
||||||
|
|
||||||
|
✅ 조사하기: [Azure Queue Storage](https://docs.microsoft.com/azure/storage/queues/storage-queues-introduction?WT.mc_id=academic-17441-jabenn)에 대해 읽어보세요.
|
||||||
|
|
||||||
|
### 파일 스토리지
|
||||||
|
|
||||||
|
파일 스토리지는 클라우드의 파일 저장소이며 모든 앱 또는 장치와 업계 표준 프로토콜을 사용하여 연결할 수 있습니다. 파일 스토리지에 파일을 쓴 다음 PC 또는 Mac의 드라이브로 마운트할 수 있습니다.
|
||||||
|
|
||||||
|
✅ 조사하기: [Azure File Storage](https://docs.microsoft.com/azure/storage/files/storage-files-introduction?WT.mc_id=academic-17441-jabenn)에 대해 읽어보세요.
|
||||||
|
|
||||||
|
## 서버리스 코드와 저장소 연결하기
|
||||||
|
|
||||||
|
이제 IoT Hub의 메시지를 저장하기 위해 function app을 Blob Storage에 연결해야 합니다. 이를 수행하는 방법에는 두 가지가 있습니다:
|
||||||
|
|
||||||
|
- function 코드 내에서 Blob 스토리지 Python SDK를 사용하여 Blob 스토리지에 연결하고 데이터를 Blob으로 작성합니다.
|
||||||
|
- 출력 함수 바인딩을 사용하여 함수의 반환 값을 Blob 스토리지에 바인딩하고 Blob을 자동으로 저장합니다.
|
||||||
|
|
||||||
|
이 강의에서는 Blob 스토리지와 어떻게 상호작용 하는지 확인하기 위해 Python SDK를 사용합니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
데이터는 다음 형식의 JSON Blob으로 저장됩니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": <device_id>,
|
||||||
|
"timestamp" : <time>,
|
||||||
|
"gps" :
|
||||||
|
{
|
||||||
|
"lat" : <latitude>,
|
||||||
|
"lon" : <longitude>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 작업 - 서버리스 코드와 저장소 연결하기
|
||||||
|
|
||||||
|
1. Azure Storage 계정을 생성합니다. 이름을 `gps<your name>`과 같이 지정합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요한 경우 [프로젝트 2의 Lesson 5에서 저장소 계정 생성을 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources)을 읽어보세요.
|
||||||
|
|
||||||
|
이전 프로젝트에서 사용한 스토리지 계정이 아직 있다면 이를 다시 사용할 수 있습니다.
|
||||||
|
|
||||||
|
> 💁 이 단원의 뒷부분에서 동일한 저장소 계정을 사용하여 Azure Functions 앱을 배포할 수 있습니다.
|
||||||
|
|
||||||
|
1. 다음 명령을 실행하여 스토리지 계정에 대한 연결 문자열을 가져옵니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az storage account show-connection-string --output table \
|
||||||
|
--name <storage_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<storage_name>`을 이전 단계에서 만든 스토리지 계정의 이름으로 변경합니다.
|
||||||
|
|
||||||
|
1. 이전 단계의 값을 사용하여 스토리지 계정 연결 문자열에 대한 새 항목을 `local.settings.json` 파일에 추가합니다. 이름을 `STORAGE_CONNECTION_STRING`으로 지정하십시오.
|
||||||
|
|
||||||
|
1. `requirements.txt` 파일에 다음 내용을 추가하여 Azure storage Pip 패키지를 설치합니다 :
|
||||||
|
|
||||||
|
```sh
|
||||||
|
azure-storage-blob
|
||||||
|
```
|
||||||
|
|
||||||
|
가상 환경에 이 파일의 패키지들을 설치합니다.
|
||||||
|
|
||||||
|
> 오류가 발생하면 다음 명령을 사용하여 가상 환경의 Pip 버전을 최신 버전으로 업그레이드한 후 다시 시도하십시오.
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> pip install --upgrade pip
|
||||||
|
> ```
|
||||||
|
|
||||||
|
1. `__init__.py` 파일의 `iot-hub-trigger`에 다음 import문을 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from azure.storage.blob import BlobServiceClient, PublicAccess
|
||||||
|
```
|
||||||
|
|
||||||
|
`json` 시스템 모듈은 JSON을 읽어오고 작성하는 데 사용됩니다. `os` 시스템 모듈은 연결 문자열을 읽어오는 데 사용됩니다. `uuid` 시스템 모듈은 GPS 판독을 위한 고유 ID를 생성하는 데 사용됩니다.
|
||||||
|
|
||||||
|
`azure.storage.blob` 패키지는 Blob 스토리지에서 작동하는 Python SDK가 포함되어 있습니다.
|
||||||
|
|
||||||
|
1. `main` 메서드 앞에 다음 도우미 함수를 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_or_create_container(name):
|
||||||
|
connection_str = os.environ['STORAGE_CONNECTION_STRING']
|
||||||
|
blob_service_client = BlobServiceClient.from_connection_string(connection_str)
|
||||||
|
|
||||||
|
for container in blob_service_client.list_containers():
|
||||||
|
if container.name == name:
|
||||||
|
return blob_service_client.get_container_client(container.name)
|
||||||
|
|
||||||
|
return blob_service_client.create_container(name, public_access=PublicAccess.Container)
|
||||||
|
```
|
||||||
|
|
||||||
|
Python blob SDK에는 컨테이너가 없는 경우 컨테이너를 만드는 도우미 메서드가 없습니다. 이 코드는 `local.settings.json` 파일(또는 클라우드에 배포된 이후 애플리케이션 설정)에서 연결 문자열을 로드한 다음, 여기에서 `BlobServiceClient` 클래스를 만들어 blob 스토리지 계정과 상호 작용합니다. 그런 다음 blob 저장소 계정에 대한 모든 컨테이너를 순회하여 제공된 이름을 가진 컨테이너를 찾습니다. 해당 컨테이너를 찾으면 해당 컨테이너와 상호 작용하여 blob을 만들 수 있는 `ContainerClient` 클래스를 반환합니다. 컨테이너가 발견되지 않으면 컨테이너가 생성되고 새 컨테이너의 클라이언트가 반환됩니다.
|
||||||
|
|
||||||
|
새 컨테이너가 생성되면 컨테이너의 Blob을 쿼리할 수 있는 공용 액세스 권한이 부여됩니다. 이것은 다음 강의에서 GPS 데이터를 지도에 시각화하는 데 사용됩니다.
|
||||||
|
|
||||||
|
1. 토양 수분과는 달리 이 코드를 사용하면 모든 이벤트를 저장할 수 있으므로 다음 코드를 `main` 함수의 `logging` 문 아래에 있는 `for event in events:` 루프 안에 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
device_id = event.iothub_metadata['connection-device-id']
|
||||||
|
blob_name = f'{device_id}/{str(uuid.uuid1())}.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 이벤트 메타데이터에서 장치 ID를 가져온 다음 이를 사용하여 Blob 이름을 만듭니다. Blob은 폴더에 저장될 수 있으며 장치 ID는 폴더 이름에 사용되므로 각 장치는 하나의 폴더에 모든 GPS 이벤트를 포함합니다. Blob 이름은 이 폴더 다음에 슬래시로 구분된 문서 이름으로, Linux 및 macOS 경로와 유사합니다(Windows와 유사하지만 Windows는 백슬래시를 사용함). 문서 이름은 Python의 `uuid` 모듈을 사용하여 생성된 고유 ID이며 파일 형식은 `json`을 사용합니다.
|
||||||
|
|
||||||
|
예를 들어, `gps-sensor` 디바이스 ID의 경우 Blob 이름은 `gps-sensor/a9487ac2-b9cf-11eb-b5cd-1e00621e3648.json`이 됩니다.
|
||||||
|
|
||||||
|
1. 이 뒤에 다음 코드를 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
container_client = get_or_create_container('gps-data')
|
||||||
|
blob = container_client.get_blob_client(blob_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 `get_or_create_container` 도우미 클래스를 사용하여 컨테이너 클라이언트를 가져온 다음 blob 이름을 사용하여 blob 클라이언트 개체를 가져옵니다. 이러한 blob 클라이언트는 기존 blob을 참조하거나, 이 경우처럼 새로운 blob을 참조할 수 있습니다.
|
||||||
|
|
||||||
|
1. 이 뒤에 다음 코드를 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
event_body = json.loads(event.get_body().decode('utf-8'))
|
||||||
|
blob_body = {
|
||||||
|
'device_id' : device_id,
|
||||||
|
'timestamp' : event.iothub_metadata['enqueuedtime'],
|
||||||
|
'gps': event_body['gps']
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
이렇게 하면 Blob Storage에 기록될 Blob의 본문이 빌드됩니다. 이는 장치 ID, 원격 분석이 IoT Hub로 전송된 시간 및 원격 분석의 GPS 좌표가 포함하는 JSON 문서입니다.
|
||||||
|
|
||||||
|
> 💁 메시지가 전송된 시간을 가져오려면 현재 시간이 아니라 메시지의 대기열에 포함된 시간을 사용하는 것이 중요합니다. Functions App이 실행되고 있지 않으면 선택되기 전에 잠시 동안 허브에 있을 수 있습니다.
|
||||||
|
|
||||||
|
1. 이 코드 아래에 다음을 추가합니다:
|
||||||
|
|
||||||
|
```python
|
||||||
|
logging.info(f'Writing blob to {blob_name} - {blob_body}')
|
||||||
|
blob.upload_blob(json.dumps(blob_body).encode('utf-8'))
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 blob이 세부 정보와 함께 작성될 예정임을 기록한 다음, blob 본문을 새 blob의 내용으로 업로드합니다.
|
||||||
|
|
||||||
|
1. Functions app을 실행합니다. 출력에서 모든 GPS 이벤트에 대해 Blob이 작성되는 것을 볼 수 있습니다.
|
||||||
|
|
||||||
|
```output
|
||||||
|
[2021-05-21T01:31:14.325Z] Python EventHub trigger processed an event: {"gps": {"lat": 47.73092, "lon": -122.26206}}
|
||||||
|
...
|
||||||
|
[2021-05-21T01:31:14.351Z] Writing blob to gps-sensor/4b6089fe-ba8d-11eb-bc7b-1e00621e3648.json - {'device_id': 'gps-sensor', 'timestamp': '2021-05-21T00:57:53.878Z', 'gps': {'lat': 47.73092, 'lon': -122.26206}}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💁 동시에 IoT Hub 이벤트 모니터를 실행하고 있지는 않은지 확인합니다.
|
||||||
|
|
||||||
|
> 💁 해당 코드는 [code/functions](../code/functions) 폴더에 있습니다.
|
||||||
|
|
||||||
|
### 작업 - 업로드된 Blob 확인
|
||||||
|
|
||||||
|
1. 생성된 blob을 보려면 스토리지 계정을 보고 관리할 수 있는 무료 도구인 [Azure Storage Explorer](https://azure.microsoft.com/features/storage-explorer/?WT.mc_id=cademic-17441-jaben)를 사용하거나 CLI에서 확인할 수 있습니다.
|
||||||
|
|
||||||
|
1. CLI를 사용하기 위해서는 우선적으로 계정 key가 필요합니다. 다음 명령어를 실행하여 해당 key를 얻습니다:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az storage account keys list --output table \
|
||||||
|
--account-name <storage_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<storage_name>`을 스토리지 계정의 이름으로 수정합니다.
|
||||||
|
|
||||||
|
`key1`의 값을 복사합니다.
|
||||||
|
|
||||||
|
1. 다음 명령어를 실행하여 컨테이너의 Blob을 나열합니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az storage blob list --container-name gps-data \
|
||||||
|
--output table \
|
||||||
|
--account-name <storage_name> \
|
||||||
|
--account-key <key1>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<storage_name>` 을 스토리지 계정의 이름으로 바꾸고, `<key1>`값을 이전 단계에서 복사한 `<key1>` 값으로 바꿉니다.
|
||||||
|
|
||||||
|
그러면 컨테이너의 모든 Blob이 나열됩니다:
|
||||||
|
|
||||||
|
```output
|
||||||
|
Name Blob Type Blob Tier Length Content Type Last Modified Snapshot
|
||||||
|
---------------------------------------------------- ----------- ----------- -------- ------------------------ ------------------------- ----------
|
||||||
|
gps-sensor/1810d55e-b9cf-11eb-9f5b-1e00621e3648.json BlockBlob Hot 45 application/octet-stream 2021-05-21T00:54:27+00:00
|
||||||
|
gps-sensor/18293e46-b9cf-11eb-9f5b-1e00621e3648.json BlockBlob Hot 45 application/octet-stream 2021-05-21T00:54:28+00:00
|
||||||
|
gps-sensor/1844549c-b9cf-11eb-9f5b-1e00621e3648.json BlockBlob Hot 45 application/octet-stream 2021-05-21T00:54:28+00:00
|
||||||
|
gps-sensor/1894d714-b9cf-11eb-9f5b-1e00621e3648.json BlockBlob Hot 45 application/octet-stream 2021-05-21T00:54:28+00:00
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 다음 명령어를 이용하여 blob 중 하나를 다운로드 합니다:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
az storage blob download --container-name gps-data \
|
||||||
|
--account-name <storage_name> \
|
||||||
|
--account-key <key1> \
|
||||||
|
--name <blob_name> \
|
||||||
|
--file <file_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
`<storage_name>`를 스토리지 계정의 이름으로 바꿉니다. `<key1>` 값을 이전 단계에서 복사한 `<key1>` 값으로 바꿉니다.
|
||||||
|
|
||||||
|
폴더 이름을 포함하여 마지막 단계 출력의 `Name` 열에 있는 전체 이름으로 `<blob_name>`을 바꿉니다. `<file_name>`을 blob을 저장할 로컬 파일의 이름으로 바꿉니다.
|
||||||
|
|
||||||
|
다운로드가 완료되면 이 JSON 파일을 VS Code에서 열 수 있으며 GPS 위치 세부 정보가 포함된 Blob이 표시됩니다:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device_id": "gps-sensor",
|
||||||
|
"timestamp": "2021-05-21T00:57:53.878Z",
|
||||||
|
"gps": { "lat": 47.73092, "lon": -122.26206 }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 업무 - Functions App을 클라우드에 배포가히
|
||||||
|
|
||||||
|
이제 Function app이 작동하므로 클라우드에 배포할 수 있습니다.
|
||||||
|
|
||||||
|
1. 이전에 생성한 스토리지 계정을 사용하여 새로운 Azure Functions app을 생성합니다. `gps-sensor-`와 같이 이름을 지정하고 끝 부분에 임의의 단어나 이름과 같은 고유 식별자를 추가합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요하다면 [프로젝트 2의 Lesson 5에서 Functions app을 생성하기 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources)을 읽어보세요.
|
||||||
|
|
||||||
|
1. `IOT_HUB_CONNECTION_STRING`과 `STORAGE_CONNECTION_STRING` 값을 Application 설정에 업로드합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요하다면 [프로젝트 2의 Lesson 5에서 Application 설정 업로드를 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---upload-your-application-settings)을 읽어보세요.
|
||||||
|
|
||||||
|
1. 로컬에 있는 Functions app을 클라우드로 배포합니다.
|
||||||
|
|
||||||
|
> ⚠️ 필요하다면 [프로젝트 2의 Lesson 5에서 Functions app 배포를 위한 지침](../../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---deploy-your-functions-app-to-the-cloud)을 읽어보세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 도전
|
||||||
|
|
||||||
|
GPS 데이터는 완벽하게 정확하지 않으며, 특히 터널과 높은 건물의 지역에서 감지되는 위치는 수 미터 정도 떨어져 있을 수 있습니다.
|
||||||
|
|
||||||
|
위성 내비게이션이 이를 어떻게 이것을 극복할 수 있을지 생각해 보세요. 위성 내비게이션이 당신의 위치를 더 잘 예측할 수 있게 해주는 데이터에는 무엇이 있습니까?
|
||||||
|
|
||||||
|
## 강의 후 퀴즈
|
||||||
|
|
||||||
|
[강의 후 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/24)
|
||||||
|
|
||||||
|
## 복습 및 자습
|
||||||
|
|
||||||
|
- 정형 데이터에 대해 [Data model page on Wikipedia](https://wikipedia.org/wiki/Data_model)에서 더 읽어보세요.
|
||||||
|
- 반정형 데이터에 대해 [Semi-structured data page on Wikipedia](https://wikipedia.org/wiki/Semi-structured_data)에서 더 읽어보세요
|
||||||
|
- 비정형 데이터에 대해 [Unstructured data page on Wikipedia](https://wikipedia.org/wiki/Unstructured_data)에서 더 읽어보세요
|
||||||
|
- Azure 스토리지와 다른 스토리지 타임에 대해 더 알아보고 싶다면 [Azure Storage documentation](https://docs.microsoft.com/azure/storage/?WT.mc_id=academic-17441-jabenn)를 참고하세요.
|
||||||
|
|
||||||
|
## 과제
|
||||||
|
|
||||||
|
[함수 바인딩 조사](../assignment.md)
|
@ -0,0 +1,140 @@
|
|||||||
|
# 마이크 및 스피커 구성 - Raspberry Pi
|
||||||
|
|
||||||
|
이 단원에서는 Raspberry Pi에 마이크와 스피커를 추가합니다.
|
||||||
|
|
||||||
|
## 하드웨어
|
||||||
|
|
||||||
|
Raspberry Pi에 연결할 마이크가 필요합니다.
|
||||||
|
|
||||||
|
Pi에는 내장 마이크가 없기 때문에 외부 마이크를 추가해야 합니다. 외부 마이크를 추가하는 방법에는 여러가지가 있습니다.
|
||||||
|
|
||||||
|
* USB 마이크
|
||||||
|
* USB 헤드셋
|
||||||
|
* USB 연결 스피커폰
|
||||||
|
* USB 연결 3.5mm 잭이 있는 오디오 어댑터 및 마이크
|
||||||
|
* [ReSpeaker 2-Mics Pi HAT](https://www.seeedstudio.com/ReSpeaker-2-Mics-Pi-HAT.html)
|
||||||
|
|
||||||
|
> 💁 Raspberry Pi에서는 블루투스 마이크가 일부 지원되지 않으므로 블루투스 마이크 또는 헤드셋이 있는 경우 오디오 페어링 또는 캡처에 문제가 있을 수 있습니다.
|
||||||
|
|
||||||
|
Raspberry Pi 장치에는 3.5mm 헤드폰 잭이 있습니다. 헤드셋 또는 스피커를 연결하기 위해 이를 사용할 수 있으며 아래 방법을 통해서도 스피커를 추가할 수 있습니다.
|
||||||
|
|
||||||
|
* 모니터 또는 TV를 통한 HDMI 오디오
|
||||||
|
* USB 스피커
|
||||||
|
* USB 헤드셋
|
||||||
|
* USB 연결 가능 스피커폰
|
||||||
|
* 3.5mm 잭 또는 JST 포트에 스피커가 부착된 [ReSpeaker 2-Mics Pi HAT](https://www.seeedstudio.com/ReSpeaker-2-Mics-Pi-HAT.html)
|
||||||
|
|
||||||
|
## 마이크와 스피커를 연결하고 구성합니다.
|
||||||
|
|
||||||
|
마이크와 스피커를 연결하고 구성해야 합니다.
|
||||||
|
|
||||||
|
### 작업 - 마이크를 연결하고 구성합시다.
|
||||||
|
|
||||||
|
1. 적절한 방법으로 마이크를 연결합니다. 예를 들어 USB 포트 중 하나를 통해 연결합니다.
|
||||||
|
|
||||||
|
1. ReSpeaker 2-Mics Pi HAT를 사용하는 경우 Grove base hat을 제거한 다음 ReSpeaker hat을 그 자리에 장착할 수 있습니다.
|
||||||
|

|
||||||
|
|
||||||
|
이 과정의 후반부에 Grove 버튼이 필요하지만, 이 모자에는 Grove base hat이 내장되어 있으므로 Grove base hat이 필요하지 않습니다.
|
||||||
|
|
||||||
|
hat이 장착되면 드라이버를 설치해야 합니다. 드라이버 설치 지침은 [Seeed getting started instructions](https://wiki.seeedstudio.com/ReSpeaker_2_Mics_Pi_HAT_Raspberry/#getting-started) 을 참고하세요.
|
||||||
|
|
||||||
|
> ⚠️ 명령어는 `git`를 사용하여 저장소를 복제합니다. Pi에 `git`이 설치되어 있지 않은 경우 다음 명령을 실행하여 설치할 수 있습니다.
|
||||||
|
>
|
||||||
|
> ```sh
|
||||||
|
> sudo apt install git --yes
|
||||||
|
> ```
|
||||||
|
|
||||||
|
1. 연결된 마이크에 대한 정보를 보려면 Pi에서 또는 VS Code 및 원격 SSH 세션을 사용하여 연결된 터미널에서 다음 명령을 실행합니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
arecord -l
|
||||||
|
```
|
||||||
|
|
||||||
|
아래와 같이 연결된 마이크 목록이 표시됩니다:
|
||||||
|
|
||||||
|
```output
|
||||||
|
pi@raspberrypi:~ $ arecord -l
|
||||||
|
**** List of CAPTURE Hardware Devices ****
|
||||||
|
card 1: M0 [eMeet M0], device 0: USB Audio [USB Audio]
|
||||||
|
Subdevices: 1/1
|
||||||
|
Subdevice #0: subdevice #0
|
||||||
|
```
|
||||||
|
|
||||||
|
연결된 마이크가 하나일 때 하나의 항목만 표시됩니다. 리눅스에서 마이크 구성이 까다로울 수 있으므로 한 개의 마이크만 사용하고 다른 마이크는 분리하는 것을 추천합니다.
|
||||||
|
|
||||||
|
카드 번호는 나중에 필요하므로 적어 두세요. 위의 출력에서 카드 번호는 1입니다.
|
||||||
|
|
||||||
|
### 작업 - 스피커를 연결하고 구성합니다.
|
||||||
|
|
||||||
|
1. 적절한 방법으로 스피커를 연결합니다.
|
||||||
|
|
||||||
|
1. 연결된 스피커에 대한 정보를 보려면 Pi에서 또는 VS Code와 원격 SSH 세션을 사용하여 연결된 터미널에서 다음 명령을 실행합니다.
|
||||||
|
```sh
|
||||||
|
aplay -l
|
||||||
|
```
|
||||||
|
|
||||||
|
아래와 같이 연결된 스피커 목록이 표시됩니다:
|
||||||
|
|
||||||
|
```output
|
||||||
|
pi@raspberrypi:~ $ aplay -l
|
||||||
|
**** List of PLAYBACK Hardware Devices ****
|
||||||
|
card 0: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
|
||||||
|
Subdevices: 8/8
|
||||||
|
Subdevice #0: subdevice #0
|
||||||
|
Subdevice #1: subdevice #1
|
||||||
|
Subdevice #2: subdevice #2
|
||||||
|
Subdevice #3: subdevice #3
|
||||||
|
Subdevice #4: subdevice #4
|
||||||
|
Subdevice #5: subdevice #5
|
||||||
|
Subdevice #6: subdevice #6
|
||||||
|
Subdevice #7: subdevice #7
|
||||||
|
card 1: M0 [eMeet M0], device 0: USB Audio [USB Audio]
|
||||||
|
Subdevices: 1/1
|
||||||
|
Subdevice #0: subdevice #0
|
||||||
|
```
|
||||||
|
|
||||||
|
헤드폰 잭이 내장돼 있어 `card 0: Headphones`이 항상 확인되는 것을 볼 수 있습니다. USB 스피커와 같은 스피커를 추가한 경우에도 이 목록은 표시됩니다.
|
||||||
|
|
||||||
|
1. 내장 헤드폰 잭에 연결된 스피커나 헤드폰이 아닌 추가 스피커를 사용하는 경우 다음 명령어를 통해 기본값으로 구성해야 합니다.
|
||||||
|
```sh
|
||||||
|
sudo nano /usr/share/alsa/alsa.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
이렇게 하면 단말기 기반 텍스트 편집기인 `nano`에서 구성 파일이 열립니다. 다음 줄을 찾을 때까지 키보드의 화살표 키를 사용하여 아래로 스크롤합니다.
|
||||||
|
|
||||||
|
```output
|
||||||
|
defaults.pcm.card 0
|
||||||
|
```
|
||||||
|
|
||||||
|
호출 후 돌아온 목록에서 사용할 카드의 카드 번호를 `0`에서 `aplay -l`로 변경합니다. 예를 들어, 위의 출력에는 `card 1: M0 [eMeet M0], 장치 0: USB Audio [USB Audio]`라는 두 번째 사운드 카드가 있습니다. 이를 사용하기 위해 다음과 같이 파일을 업데이트합니다.
|
||||||
|
|
||||||
|
```output
|
||||||
|
defaults.pcm.card 1
|
||||||
|
```
|
||||||
|
|
||||||
|
이 값을 적절한 카드 번호로 설정합니다. 키보드의 화살표 키를 사용하여 숫자로 이동한 다음 텍스트 파일을 편집할 때 일반적으로 새 숫자를 삭제하고 입력할 수 있습니다.
|
||||||
|
|
||||||
|
1. `Ctrl+x`를 눌러 변경 내용을 저장하고 파일을 닫습니다. `y`를 눌러 파일을 저장한 다음 `return`을 눌러 파일 이름을 선택합니다.
|
||||||
|
|
||||||
|
### 작업 - 마이크와 스피커를 테스트합니다
|
||||||
|
|
||||||
|
1. 다음 명령을 실행하여 마이크를 통해 5초간의 오디오를 녹음합니다.:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
arecord --format=S16_LE --duration=5 --rate=16000 --file-type=wav out.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
이 명령이 실행되는 동안 말하기, 노래하기, 비트박스, 악기 연주 또는 하고싶은 것을 하며 마이크에 소리를 내십시오.
|
||||||
|
|
||||||
|
1. 5초 후에 녹화가 중지됩니다. 다음 명령을 실행하여 오디오를 재생합니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
aplay --format=S16_LE --rate=16000 out.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
스피커를 통해 audio bing이 재생되는 소리가 들립니다. 필요에 따라 스피커의 출력 볼륨을 조정합니다.
|
||||||
|
|
||||||
|
1. 내장된 마이크 포트의 볼륨을 조절하거나 마이크의 게인을 조절해야 할 경우 `alsamixer` 유틸리티를 사용할 수 있습니다. 이 유틸리티에 대한 자세한 내용은 [Linux alsamixer man page](https://linux.die.net/man/1/alsamixer) 에서 확인할 수 있습니다.
|
||||||
|
|
||||||
|
1. 오디오를 재생할 때 오류가 발생하면 `alsa.conf` 파일에서 `defaults.pcm.card`로 설정한 카드를 확인합니다.
|
@ -0,0 +1,130 @@
|
|||||||
|
# 타이머 설정 및 음성 피드백 제공
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> [Nitya Narasimhan](https://github.com/nitya)의 스케치노트. 클릭하여 크게 보세요.
|
||||||
|
|
||||||
|
## 강의 전 퀴즈
|
||||||
|
|
||||||
|
[강의 전 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/45)
|
||||||
|
|
||||||
|
## 도입
|
||||||
|
|
||||||
|
스마트 어시스턴트는 단방향 통신 장치가 아닙니다. 그들에게 말을 걸면 그들은 대답합니다.
|
||||||
|
|
||||||
|
"Alexa, 3분짜리 타이머 설정해줘"
|
||||||
|
|
||||||
|
"네. 3분 타이머 설정했습니다."
|
||||||
|
|
||||||
|
지난 두 번의 수업에서 여러분은 강의를 듣고 음성을 통해 텍스트를 생성하는 방법을 배웠고, 생성한 텍스트에서 타이머 설정 요청에 관한 내용을 추출했습니다. 이 수업에서는 IoT 장치에서 타이머를 설정하고, 사용자에게 타이머 설정이 완료됨을 확인하는 음성안내를 송출한 후, 타이머가 종료되면 알람을 울리는 방법을 배울 것 입니다.
|
||||||
|
|
||||||
|
이 강의에서는 다음을 다룹니다.:
|
||||||
|
|
||||||
|
* [텍스트에서 음성으로](#텍스트에서-음성으로)
|
||||||
|
* [타이머 설정하기](#타이머-설정하기)
|
||||||
|
* [텍스트를 음성으로 변환하기](#텍스트를-음성으로-변환하기)
|
||||||
|
|
||||||
|
## 텍스트에서 음성으로
|
||||||
|
|
||||||
|
제목에서부터 알다시피 텍스트를 음성으로 변환하는 과정입니다. 기본 원칙은 텍스트의 단어를 구성하는 소리(음소)로 나누고, 사전 녹음된 오디오나 AI모델에서 생성된 오디오를 사용하여 하나의 오디오로 연결합니다.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
텍스트 음성 변환 시스템은 일반적으로 3단계로 구성됩니다.
|
||||||
|
|
||||||
|
* 텍스트 분석
|
||||||
|
* 언어 분석
|
||||||
|
* 파형 발생
|
||||||
|
|
||||||
|
### 텍스트 분석
|
||||||
|
|
||||||
|
텍스트 분석에는 제공된 문장을 가져와서 음성을 생성하는 데 필요한 단어로 변환하는 작업이 포함됩니다. 예를 들어 "Hello wolrd" 를 변환하면 텍스트 분석 없이 두 단어를 음성으로 변환할 수 있습니다. 그러나 "1234"는 문맥에 따라 "천 이백 삼시 사" 혹은 "일 이 삼 사" 로 변환해야 할 수 있습니다. "나는 1234개의 사과를 가지고 있다"의 경우 문맥상 "천 이백 삼십 사"로 감지되지만, "아이가 1234ㄹ를 세었다"의 경우 "일 이 삼 사"가 됩니다.
|
||||||
|
|
||||||
|
생성된 단어는 언어 뿐만 아니라 해당 언어의 사용 환경에 따라 다릅니다. 예를 들면, 미국영어에서 "120"은 "One hundred twenty" 이지만, 영국에서는 "One hundred and twenty"으로 hundreds 뒤에 "and"를 사용합니다.
|
||||||
|
|
||||||
|
✅ 텍스트 분석이 필요한 다른 예로는 inch의 in을 발음하는 짧은 음, saint와 street의 짧은 형태의 "st"등이 있습니다. 여러분은 문맥 상 모호한 언어로 구성된 다른 예시들이 생각 나시나요?
|
||||||
|
|
||||||
|
+) 한국어 문맥 상 "난 네 말이 궁금해" 에서 동물을 의미하는 지 대화를 의미하는 지 등의 동음이의어이지 않을까 생각합니다.
|
||||||
|
|
||||||
|
문맥상 단어가 명확해지면 언어 분석을 위해 전송됩니다.
|
||||||
|
|
||||||
|
### 언어 분석
|
||||||
|
|
||||||
|
언어 분석은 단어들을 음소로 나눕니다. 음소는 사용된 문자 뿐만 아니라 단어의 다른 문자에도 기반을 두고 있습니다. 예를들어 영어에서 'car'와 'care'에서의 'a'소리가 다릅니다. 영어는 알파벳의 26개 문자에 대하여 44개의 서로 다른 음소를 가지고 있으며, 일부는 'circle'와 'serpent'처럼 다른 문자이지만 동일한 음소로 사용되는 경우도 존재합니다.
|
||||||
|
|
||||||
|
✅ 생각 해 봅시다 : 한국어 음소는 무엇이 있나요?
|
||||||
|
|
||||||
|
일단 단어들이 음소로 변환되면, 이 음소들은 문맥에 따라서 음조나 발음 시간을 조절하면서 억양을 지원하기 위한 추가적인 데이터가 필요합니다. 한 예로 영어에서 마지막 단어에 대한 음높이 증가는 질문을 의미합니다. 이를 바탕으로 문장을 질문으로 변환하는 데 사용할 수 있습니다.
|
||||||
|
|
||||||
|
예를 들어, "You have an apple"이라는 문장은 상대방이 사과를 가지고 있다는 것을 나타내는 문장입니다. 만약 마지막 'apple' 단어의 끝음이 올라간다면 "You have an apple?"의 질문 형태가 됩니다. 언어 분석 시 마지막 단어의 음이 올라가는 것을 파악하면 물음표를 사용하여 질문임을 표시해야합니다.
|
||||||
|
|
||||||
|
일단 음소가 생성되면, 오디오 출력을 생성하기 위한 파형 생성을 위해 전송될 수 있습니다.
|
||||||
|
|
||||||
|
### 파형 발생
|
||||||
|
|
||||||
|
최초의 전자 텍스트 음성 변환 시스템은 각 음소에 단일 오디오 녹음을 사용하여 매우 단조로운 로봇 음성으로 구현되었습니다. 이들은 소리 데이터베이스에서 로드되고 오디오를 만들기 위해 연결됩니다.
|
||||||
|
|
||||||
|
✅ 생각 해 봅시다 : 초기 음성 합성 시스템의 오디오 녹음을 찾아봅시다. 스마트 어시스턴트에 사용되는 것과 같은 최신 음성 합성과 비교 해 봅시다.
|
||||||
|
|
||||||
|
보다 최신의 파형 생성은 딥러닝을 사용하여 구축된 머신 러닝 모델을 사용하여 인간과 구별 할 수 없는 더 자연스러운 소리를 생성합니다.
|
||||||
|
|
||||||
|
> 💁 이러한 머신 러닝 모델 중 일부는 실제 사람이 말하는 것 처럼 들리기 위해 전이 학습을 사용하여 재학습 될 수 있습니다. 즉, 은행에서 점점 더 시도하고 있는 음성 보안 시스템은 여러분의 목소리를 몇 분 녹음한 사람은 누구나 여러분을 사칭할 수 있기 때문에 좋은 방법이 아니라고 생각합니다.
|
||||||
|
|
||||||
|
이러한 대형 머신 러닝 모델은 세 단계를 모두 end-to-end 음성 합성기로 결합하도록 학습되고 있습니다.
|
||||||
|
|
||||||
|
## 타이머 설정하기
|
||||||
|
|
||||||
|
타이머를 설정하려면 IoT 장치가 서버리스 코드를 사용하여 만든 REST 엔드포인트를 호출한 후, 결과 시간을 사용하여 타이머를 설정합니다.
|
||||||
|
|
||||||
|
### 작업 - 서버리스 함수를 호출하여 타이머 시간을 가져옵시다.
|
||||||
|
|
||||||
|
아래 안내에 따라 IoT 장치에서 REST 끝 점을 호출하고 요청한 시간 동안의 타이머를 설정 해 줍니다.:
|
||||||
|
|
||||||
|
* [Arduino - Wio Terminal](../wio-terminal-set-timer.md)
|
||||||
|
* [Single-board computer - Raspberry Pi/Virtual IoT device](single-board-computer-set-timer.ko.md)
|
||||||
|
|
||||||
|
## 텍스트를 음성으로 변환하기
|
||||||
|
|
||||||
|
음성을 텍스트로 변환하는 데 사용한 것과 동일한 음성 서비스를 사용하여 텍스트를 다시 음성으로 변환할 수 있습니다. 이는 IoT 장치의 스피커를 통해 재생할 수 있습니다. 변환하고자 하는 텍스트는 필요한 오디오 유형(ex: 샘플링 정도)정보와 함께 음성 서비스로 전송되고 오디오가 포함된 이진 데이터가 반환됩니다.
|
||||||
|
|
||||||
|
해당 요청을 보낼 때, 음성 합성 응용 프로그램을 위한 XML 기반 마크업 언어인 *Speech Synthesis Markup Language*(SSML)를 사용하여 전송합니다. 이것은 변환될 텍스트 뿐만 아니라 텍스트의 언어, 사용할 음성을 정의하며 텍스트의 일부 또는 전부에 대한 말한느 속도, 볼륨, 피치 등을 정의하는 데 사용될 수 있습니다.
|
||||||
|
|
||||||
|
예를 들어, 이 SSML은 "Your 3 minute 5 second time has been set" 라는 텍스트를 `en-GB-MiaNeural`이라는 영국 언어 음성을 사용하여 음성으로 변환하는 요청을 정의합니다.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<speak version='1.0' xml:lang='en-GB'>
|
||||||
|
<voice xml:lang='en-GB' name='en-GB-MiaNeural'>
|
||||||
|
Your 3 minute 5 second time has been set
|
||||||
|
</voice>
|
||||||
|
</speak>
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💁 대부분의 텍스트 음성 시스템은 영국 억양 영어 음성, 뉴질랜드 억양 영어 음성 등 다양한 언어에 대한 다양한 음성을 가지고 있습니다.
|
||||||
|
|
||||||
|
### 작업 - 텍스트를 음성으로 변환 해 봅시다.
|
||||||
|
|
||||||
|
IoT 장치를 사용하여 텍스트를 음성으로 변환하려면 아래 가이드를 사용하십시오.
|
||||||
|
|
||||||
|
* [Arduino - Wio Terminal](../wio-terminal-text-to-speech.md)
|
||||||
|
* [Single-board computer - Raspberry Pi](pi-text-to-speech.ko.md)
|
||||||
|
* [Single-board computer - Virtual device](../virtual-device-text-to-speech.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 도전
|
||||||
|
|
||||||
|
SSML은 특정 단어를 강조하거나, 잠깐의 텀을 두거나, 음높이를 변경하는 등 단어를 말하는 방식을 변경하는 방법을 가지고 있습니다. IoT 장치에서 다른 SSML을 전송하고 출력을 비교하면서 이 중 몇가지를 시도해봅시다. SSML에 대한 자세한 내용은 [World Wide Web Consortium의 음성 합성 마크업 언어(SSML) Version 1.1](https://www.w3.org/TR/speech-synthesis11/))에서 확인할 수 있습니다.
|
||||||
|
|
||||||
|
## 강의 후 퀴즈
|
||||||
|
|
||||||
|
[강의 후 퀴즈](https://black-meadow-040d15503.1.azurestaticapps.net/quiz/46)
|
||||||
|
|
||||||
|
## 복습 및 독학
|
||||||
|
|
||||||
|
* 음성 합성에 대한 자세한 내용은 [speech synthesis page on Wikipedia](https://wikipedia.org/wiki/Speech_synthesis) 에서 확인 하세요.
|
||||||
|
* 범죄자들이 음성 합성을 사용하여 [가짜 목소리로 돈을 훔치는 것에 대한 BBC뉴스](https://www.bbc.com/news/technology-48908736) 기사에서 확인 해 보세요.
|
||||||
|
* [TikTok 플랫폼이 성우들의 동의 없이 AI를 사용하여 그들의 목소리를 악의적으로 사용하였다.](https://www.vice.com/en/article/z3xqwj/this-tiktok-lawsuit-is-highlighting-how-ai-is-screwing-over-voice-actors) 와 관련된 기사를 확인 해 보세요.
|
||||||
|
|
||||||
|
## 과제
|
||||||
|
|
||||||
|
[타이머 취소하기](assignment.ko.md)
|
@ -0,0 +1,140 @@
|
|||||||
|
# 텍스트 음성 변환 - Raspberry Pi
|
||||||
|
|
||||||
|
이 단원에서는 음성 서비스를 사용하여 텍스트를 음성으로 변환하는 코드를 작성합니다.
|
||||||
|
|
||||||
|
## 음성 서비스를 사용하여 텍스트를 음성으로 변환 해봅시다.
|
||||||
|
|
||||||
|
텍스트는 REST API를 사용하여 음성 서비스로 전송되어 IoT 장치에서 재생할 수 있는 오디오 파일로 음성을 얻을 수 있습니다. 음성을 요청할 때 다양한 음성을 사용하여 변환된 음성을 생성할 수 있으므로 사용할 voice를 제공해야 합니다.
|
||||||
|
|
||||||
|
각 언어는 다양한 voice를 지원하며, 음성 서비스에 대해 REST를 요청하여 각 언어에 대해 지원되는 voice 목록을 얻을 수 있습니다.
|
||||||
|
|
||||||
|
### 작업 - voice를 얻어 봅시다.
|
||||||
|
|
||||||
|
1. VS Code에서 `smart-timer` 프로젝트를 열어주세요.
|
||||||
|
|
||||||
|
1. 언어에 대한 voice 목록을 요청하려면 `say`함수 뒤에 아래 코드를 추가하십시오.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_voice():
|
||||||
|
url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': 'Bearer ' + get_access_token()
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
voices_json = json.loads(response.text)
|
||||||
|
|
||||||
|
first_voice = next(x for x in voices_json if x['Locale'].lower() == language.lower() and x['VoiceType'] == 'Neural')
|
||||||
|
return first_voice['ShortName']
|
||||||
|
|
||||||
|
voice = get_voice()
|
||||||
|
print(f'Using voice {voice}')
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 음성 서비스를 사용하여 voice 리스트를 가져오는 `get_voice`라는 함수를 정의합니다. 이후 사용중인 언어와 일치하는 첫 번째 voice를 찾습니다.
|
||||||
|
|
||||||
|
이후 이 기능을 호출하여 첫 번째 voice를 저장하면 음성 이름이 console에 출력됩니다. 이 voice는 한 번 요청할 수 있으며 텍스트를 음성으로 변환하기 위해 모든 호출에 사용되는 값입니다.
|
||||||
|
|
||||||
|
> 💁 지원되는 voice의 전체 목록은 [Microsoft Docs 언어 및 음성 지원 설명서](https://docs.microsoft.com/azure/cognitive-services/speech-service/language-support?WT.mc_id=academic-17441-jabenn#text-to-speech)에서 확인할 수 있습니다. 특정 voice를 사용하려면 이 기능을 제거하고 설명서에서 voice를 해당 voice의 명칭으로 하드코딩 할 수 있습니다.
|
||||||
|
> 예시 :
|
||||||
|
> ```python
|
||||||
|
> voice = 'hi-IN-SwaraNeural'
|
||||||
|
> ```
|
||||||
|
|
||||||
|
### 작업 - 텍스트를 음성으로 변환
|
||||||
|
|
||||||
|
1. 아래에서 음성 서비스에서 검색할 오디오 형식에 대한 상수를 정의합니다. 오디오를 요청할 때 다양한 형식으로 요청할 수 있습니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
playback_format = 'riff-48khz-16bit-mono-pcm'
|
||||||
|
```
|
||||||
|
|
||||||
|
하드웨어에 따라 사용할 수 있는 형식이 다릅니다. 오디오를 재생할 때 `Invalid sample rate`오류가 발생하면 이 값을 다른 값으로 변경하면 됩니다. 지원되는 목록은 [Microsoft Docs의 Text to speech REST API 설명서](https://docs.microsoft.com/azure/cognitive-services/speech-service/rest-text-to-speech?WT.mc_id=academic-17441-jabenn#audio-outputs) 에서 확인할 수 있습니다. `riff` 형식의 오디오를 사용해야 하며, 시도할 값은 `riff-16khz-16bit-mono-pcm`, `riff-24khz-16bit-mono-pcm` 그리고 `riff-48khz-16bit-mono-pcm`입니다.
|
||||||
|
|
||||||
|
1. 아래 코드를 통해 음성 서비스 REST API를 사용하여 텍스트를 음성으로 변환하는 `get_speech`함수를 선언합니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_speech(text):
|
||||||
|
```
|
||||||
|
|
||||||
|
1. `get_speech` 함수에서 호출할 URL과 전달할 헤더를 정의합니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/v1'
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': 'Bearer ' + get_access_token(),
|
||||||
|
'Content-Type': 'application/ssml+xml',
|
||||||
|
'X-Microsoft-OutputFormat': playback_format
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
생성된 액세스 토큰을 사용하도록 헤더를 설정하고 콘텐츠를 SSML로 설정하며 필요한 오디오 형식을 정의합니다.
|
||||||
|
|
||||||
|
1. 아래 코드를 통해 REST API로 보낼 SSML을 정의합니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>'
|
||||||
|
ssml += f'<voice xml:lang=\'{language}\' name=\'{voice}\'>'
|
||||||
|
ssml += text
|
||||||
|
ssml += '</voice>'
|
||||||
|
ssml += '</speak>'
|
||||||
|
```
|
||||||
|
|
||||||
|
이 SSML은 변환할 텍스트와 함께 사용할 언어와 voice를 설정합니다.
|
||||||
|
|
||||||
|
1. 마지막으로 REST를 요청하고 이진 오디오 데이터를 반환하는 코드를 이 함수에 추가합니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
response = requests.post(url, headers=headers, data=ssml.encode('utf-8'))
|
||||||
|
return io.BytesIO(response.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 작업 - 오디오를 재생해봅시다
|
||||||
|
|
||||||
|
1. `get_speech` 함수 아래에 REST API 호출에 의해 반환된 오디오를 재생하는 새 함수를 정의합니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def play_speech(speech):
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 이 함수에 전달되는 `speech`는 REST API에서 반환되는 이진 오디오 데이터입니다. 다음 코드를 사용하여 이를 파형 파일로 열고 PyAudio로 전달하여 오디오를 재생합니다
|
||||||
|
```python
|
||||||
|
def play_speech(speech):
|
||||||
|
with wave.open(speech, 'rb') as wave_file:
|
||||||
|
stream = audio.open(format=audio.get_format_from_width(wave_file.getsampwidth()),
|
||||||
|
channels=wave_file.getnchannels(),
|
||||||
|
rate=wave_file.getframerate(),
|
||||||
|
output_device_index=speaker_card_number,
|
||||||
|
output=True)
|
||||||
|
|
||||||
|
data = wave_file.readframes(4096)
|
||||||
|
|
||||||
|
while len(data) > 0:
|
||||||
|
stream.write(data)
|
||||||
|
data = wave_file.readframes(4096)
|
||||||
|
|
||||||
|
stream.stop_stream()
|
||||||
|
stream.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 오디오를 캡처하는 것과 동일한 PyAudio 스트림을 사용합니다. 여기서 차이점은 오디오 데이터에서 데이터가 읽혀지고 출력 스트림으로 설정 된 스트림으로 푸시된다는 것입니다.
|
||||||
|
|
||||||
|
샘플링 정도와 같은 스트림 세부 사항을 하드 코딩하는 대신 오디오 데이터로부터 읽다.
|
||||||
|
|
||||||
|
1. `say` 함수의 내용을 다음과 같이 바꿉니다.
|
||||||
|
|
||||||
|
```python
|
||||||
|
speech = get_speech(text)
|
||||||
|
play_speech(speech)
|
||||||
|
```
|
||||||
|
|
||||||
|
이 코드는 텍스트를 이진 오디오 데이터로 음성으로 변환하고 오디오를 재생합니다.
|
||||||
|
|
||||||
|
1. 앱을 실행하고 function 앱도 실행 중인지 확인합니다. 타이머를 설정하면 타이머가 설정되었다는 음성 응답이 들리고, 타이머의 시간이 완료되면 다른 음성 응답이 들립니다.
|
||||||
|
`Invalid sample rate` 오류가 발생하면 위에서 설명한 대로 `playback_format`을 변경하십시오.
|
||||||
|
|
||||||
|
> 💁 [code-spoken-response/pi](code-spoken-response/pi) 폴더에서 코드를 확인할 수 있습니다.
|
||||||
|
|
||||||
|
😀 타이머를 만들었어요! 야호!
|
Loading…
Reference in new issue